Completed
Push — master ( 67bebb...73147e )
by Doug
35:04
created

VolumePacker::getOrientationForItem()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 31
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 11
c 3
b 0
f 0
nc 4
nop 11
dl 0
loc 31
ccs 11
cts 11
cp 1
crap 3
rs 9.9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * Box packing (3D bin packing, knapsack problem).
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace DVDoug\BoxPacker;
10
11
use Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerAwareTrait;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\NullLogger;
15
use function count;
16
use function max;
17
use function min;
18
19
/**
20
 * Actual packer.
21
 *
22
 * @author Doug Wright
23
 */
24
class VolumePacker implements LoggerAwareInterface
25
{
26
    /**
27
     * The logger instance.
28
     *
29
     * @var LoggerInterface
30
     */
31
    protected $logger;
32
33
    /**
34
     * Box to pack items into.
35
     *
36
     * @var Box
37
     */
38
    protected $box;
39
40
    /**
41
     * @var int
42
     */
43
    protected $boxWidth;
44
45
    /**
46
     * @var int
47
     */
48
    protected $boxLength;
49
50
    /**
51
     * List of items to be packed.
52
     *
53
     * @var ItemList
54
     */
55
    protected $items;
56
57
    /**
58
     * List of items temporarily skipped to be packed.
59
     *
60
     * @var array
61
     */
62
    protected $skippedItems = [];
63
64
    /**
65
     * Remaining weight capacity of the box.
66
     *
67
     * @var int
68
     */
69
    protected $remainingWeight;
70
71
    /**
72
     * Whether the box was rotated for packing.
73
     *
74
     * @var bool
75
     */
76
    protected $boxRotated = false;
77
78
    /**
79
     * @var PackedLayer[]
80
     */
81
    protected $layers = [];
82
83
    /**
84
     * Whether the packer is in look-ahead mode (i.e. working ahead of the main packing).
85
     *
86
     * @var bool
87
     */
88
    protected $lookAheadMode = false;
89
90
    /**
91
     * @var OrientatedItemFactory
92
     */
93
    private $orientatedItemFactory;
94
95
    /**
96
     * Constructor.
97
     *
98
     * @param Box      $box
99
     * @param ItemList $items
100
     */
101 39
    public function __construct(Box $box, ItemList $items)
102
    {
103 39
        $this->box = $box;
104 39
        $this->items = $items;
105
106 39
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
107 39
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
108 39
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
109 39
        $this->logger = new NullLogger();
110
111
        // we may have just rotated the box for packing purposes, record if we did
112 39
        if ($this->box->getInnerWidth() !== $this->boxWidth || $this->box->getInnerLength() !== $this->boxLength) {
113 13
            $this->boxRotated = true;
114
        }
115
116 39
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
117 39
    }
118
119
    /**
120
     * Sets a logger.
121
     *
122
     * @param LoggerInterface $logger
123
     */
124 12
    public function setLogger(LoggerInterface $logger): void
125
    {
126 12
        $this->logger = $logger;
127 12
        $this->orientatedItemFactory->setLogger($logger);
128 12
    }
129
130
    /**
131
     * @internal
132
     * @param bool $lookAhead
133
     */
134 13
    public function setLookAheadMode(bool $lookAhead): void
135
    {
136 13
        $this->lookAheadMode = $lookAhead;
137 13
    }
138
139
    /**
140
     * Pack as many items as possible into specific given box.
141
     *
142
     * @return PackedBox packed box
143
     */
144 39
    public function pack(): PackedBox
145
    {
146 39
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);
147
148 39
        while (count($this->items) > 0) {
149 39
            $layerStartDepth = $this->getCurrentPackedDepth();
150 39
            $this->packLayer($layerStartDepth, $this->boxWidth, $this->boxLength, $this->box->getInnerDepth() - $layerStartDepth);
151
        }
152
153 39
        if ($this->boxRotated) {
154 13
            $this->rotateLayersNinetyDegrees();
155
        }
156
157 39
        if (!$this->lookAheadMode) {
158 39
            $this->stabiliseLayers();
159
        }
160
161 39
        $this->logger->debug('done with this box');
162
163 39
        return new PackedBox($this->box, $this->getPackedItemList());
164
    }
165
166
    /**
167
     * Pack items into an individual vertical layer.
168
     *
169
     * @param int $startDepth
170
     * @param int $widthLeft
171
     * @param int $lengthLeft
172
     * @param int $depthLeft
173
     */
174 39
    protected function packLayer(int $startDepth, int $widthLeft, int $lengthLeft, int $depthLeft): void
175
    {
176 39
        $this->layers[] = $layer = new PackedLayer();
177 39
        $prevItem = null;
178 39
        $x = $y = $rowWidth = $rowLength = $layerDepth = 0;
179
180 39
        while (count($this->items) > 0) {
181 39
            $itemToPack = $this->items->extract();
182
183
            //skip items that are simply too heavy or too large
184 39
            if (!$this->checkNonDimensionalConstraints($itemToPack)) {
185 3
                continue;
186
            }
187
188 39
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, !$this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $startDepth);
189
190 39
            if ($orientatedItem instanceof OrientatedItem) {
191 38
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth);
192 38
                $layer->insert($packedItem);
193 38
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
194 38
                $widthLeft -= $orientatedItem->getWidth();
195
196 38
                $rowWidth += $orientatedItem->getWidth();
197 38
                $rowLength = max($rowLength, $orientatedItem->getLength());
198 38
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
199
200
                //allow items to be stacked in place within the same footprint up to current layer depth
201 38
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
202 38
                $this->tryAndStackItemsIntoSpace($layer, $prevItem, $this->items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth(), $rowLength);
203 38
                $x += $orientatedItem->getWidth();
204
205 38
                $prevItem = $packedItem;
206 38
                if (count($this->items) === 0) {
207 38
                    $this->rebuildItemList();
208
                }
209 34
            } elseif (count($layer->getItems()) === 0) { // zero items on layer
210 17
                $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
211 17
                continue;
212 32
            } elseif ($widthLeft > 0 && count($this->items) > 0) { // skip for now, move on to the next item
213 27
                $this->logger->debug("doesn't fit, skipping for now");
214 27
                $this->skippedItems[] = $itemToPack;
215 32
            } elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength(), $itemToPack->getDepth())) {
216 32
                $this->logger->debug('No more fit in width wise, resetting for new row');
217 32
                $widthLeft += $rowWidth;
218 32
                $lengthLeft -= $rowLength;
219 32
                $y += $rowLength;
220 32
                $x = $rowWidth = $rowLength = 0;
221 32
                $this->skippedItems[] = $itemToPack;
222 32
                $this->rebuildItemList();
223 32
                $prevItem = null;
224 32
                continue;
225
            } else {
226 29
                $this->logger->debug('no items fit, so starting next vertical layer');
227 29
                $this->skippedItems[] = $itemToPack;
228 29
                $this->rebuildItemList();
229 29
                return;
230
            }
231
        }
232 39
    }
233
234
    /**
235
     * During packing, it is quite possible that layers have been created that aren't physically stable
236
     * i.e. they overhang the ones below.
237
     *
238
     * This function reorders them so that the ones with the greatest surface area are placed at the bottom
239
     */
240 39
    public function stabiliseLayers(): void
241
    {
242 39
        $stabiliser = new LayerStabiliser();
243 39
        $this->layers = $stabiliser->stabilise($this->layers);
244 39
    }
245
246
    /**
247
     * @param Item            $itemToPack
248
     * @param PackedItem|null $prevItem
249
     * @param ItemList        $nextItems
250
     * @param bool            $isLastItem
251
     * @param int             $maxWidth
252
     * @param int             $maxLength
253
     * @param int             $maxDepth
254
     * @param int             $rowLength
255
     * @param int             $x
256
     * @param int             $y
257
     * @param int             $z
258
     *
259
     * @return OrientatedItem|null
260
     */
261 39
    protected function getOrientationForItem(
262
        Item $itemToPack,
263
        ?PackedItem $prevItem,
264
        ItemList $nextItems,
265
        bool $isLastItem,
266
        int $maxWidth,
267
        int $maxLength,
268
        int $maxDepth,
269
        int $rowLength,
270
        int $x,
271
        int $y,
272
        int $z
273
    ): ?OrientatedItem {
274 39
        $this->logger->debug(
275 39
            "evaluating item {$itemToPack->getDescription()} for fit",
276
            [
277 39
                'item' => $itemToPack,
278
                'space' => [
279 39
                    'maxWidth' => $maxWidth,
280 39
                    'maxLength' => $maxLength,
281 39
                    'maxDepth' => $maxDepth,
282
                ],
283
            ]
284
        );
285
286 39
        $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
287 39
        $prevPackedItemList = $itemToPack instanceof ConstrainedPlacementItem ? $this->getPackedItemList() : new PackedItemList(); // don't calculate it if not going to be used
288
289 39
        $orientatedItemDecision = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth, $rowLength, $x, $y, $z, $prevPackedItemList);
290
291 39
        return $orientatedItemDecision;
292
    }
293
294
    /**
295
     * Figure out if we can stack the next item vertically on top of this rather than side by side
296
     * Used when we've packed a tall item, and have just put a shorter one next to it.
297
     *
298
     * @param PackedLayer     $layer
299
     * @param PackedItem|null $prevItem
300
     * @param ItemList        $nextItems
301
     * @param int             $maxWidth
302
     * @param int             $maxLength
303
     * @param int             $maxDepth
304
     * @param int             $x
305
     * @param int             $y
306
     * @param int             $z
307
     */
308 38
    protected function tryAndStackItemsIntoSpace(
309
        PackedLayer $layer,
310
        ?PackedItem $prevItem,
311
        ItemList $nextItems,
312
        int $maxWidth,
313
        int $maxLength,
314
        int $maxDepth,
315
        int $x,
316
        int $y,
317
        int $z,
318
        int $rowLength
319
    ): void {
320 38
        while (count($this->items) > 0 && $this->checkNonDimensionalConstraints($this->items->top())) {
321 37
            $stackedItem = $this->getOrientationForItem(
322 37
                $this->items->top(),
323
                $prevItem,
324
                $nextItems,
325 37
                $this->items->count() === 1,
326
                $maxWidth,
327
                $maxLength,
328
                $maxDepth,
329
                $rowLength,
330
                $x,
331
                $y,
332
                $z
333
            );
334 37
            if ($stackedItem) {
335 5
                $this->remainingWeight -= $this->items->top()->getWeight();
336 5
                $layer->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $z));
337 5
                $this->items->extract();
338 5
                $maxDepth -= $stackedItem->getDepth();
339 5
                $z += $stackedItem->getDepth();
340
            } else {
341 37
                break;
342
            }
343
        }
344 38
    }
345
346
    /**
347
     * As well as purely dimensional constraints, there are other constraints that need to be met
348
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
349
     *
350
     * @param Item $itemToPack
351
     *
352
     * @return bool
353
     */
354 39
    protected function checkNonDimensionalConstraints(Item $itemToPack): bool
355
    {
356 39
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
357
358 39
        if ($itemToPack instanceof ConstrainedItem) {
359 1
            return $weightOK && $itemToPack->canBePackedInBox($this->getPackedItemList(), $this->box);
360
        }
361
362 39
        return $weightOK;
363
    }
364
365
    /**
366
     * Reintegrate skipped items into main list.
367
     */
368 38
    protected function rebuildItemList(): void
369
    {
370 38
        $this->items = ItemList::fromArray(array_merge($this->skippedItems, iterator_to_array($this->items)), true);
371
        /*while(count($this->items) > 0) {
372
            $newList->insert($this->items->extract());
373
        }
374
        $this->items = $newList;*/
375 38
        $this->skippedItems = [];
376 38
    }
377
378
    /**
379
     * Swap back width/length of the packed items to match orientation of the box if needed.
380
     */
381 13
    protected function rotateLayersNinetyDegrees(): void
382
    {
383 13
        foreach ($this->layers as $i => $originalLayer) {
384 13
            $newLayer = new PackedLayer();
385 13
            foreach ($originalLayer->getItems() as $item) {
386 13
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
387 13
                $newLayer->insert($packedItem);
388
            }
389 13
            $this->layers[$i] = $newLayer;
390
        }
391 13
    }
392
393
    /**
394
     * Are there items left to pack?
395
     *
396
     * @return bool
397
     */
398 39
    protected function hasItemsLeftToPack(): bool
399
    {
400 39
        return count($this->skippedItems) + count($this->items) > 0;
401
    }
402
403
    /**
404
     * Generate a single list of items packed.
405
     *
406
     * @return PackedItemList
407
     */
408 39
    protected function getPackedItemList(): PackedItemList
409
    {
410 39
        $packedItemList = new PackedItemList();
411 39
        foreach ($this->layers as $layer) {
412 39
            foreach ($layer->getItems() as $packedItem) {
413 39
                $packedItemList->insert($packedItem);
414
            }
415
        }
416
417 39
        return $packedItemList;
418
    }
419
420
    /**
421
     * Return the current packed depth.
422
     *
423
     * @return int
424
     */
425 39
    protected function getCurrentPackedDepth(): int
426
    {
427 39
        $depth = 0;
428 39
        foreach ($this->layers as $layer) {
429 29
            $depth += $layer->getDepth();
430
        }
431
432 39
        return $depth;
433
    }
434
}
435