Passed
Push — 2.x-dev ( 9c54be...ba24d3 )
by Doug
04:59
created

VolumePacker::tryAndStackItemsIntoSpace()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 21
c 0
b 0
f 0
nc 3
nop 10
dl 0
loc 34
ccs 21
cts 21
cp 1
crap 4
rs 9.584

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