Passed
Push — repack_layers_for_new_height ( 96fee3...7073dd )
by Doug
01:59
created

VolumePacker::tryAndStackItemsIntoSpace()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 32
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 18
cts 18
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 28
nc 3
nop 9
crap 4

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