Passed
Push — behat ( a74de5...4872f1 )
by Doug
02:39
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
     * Constructor.
71
     *
72
     * @param Box      $box
73
     * @param ItemList $items
74
     */
75 15
    public function __construct(Box $box, ItemList $items)
76
    {
77 15
        $this->box = $box;
78 15
        $this->items = $items;
79
80 15
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
81 15
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
82 15
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
83 15
        $this->skippedItems = new ItemList();
84 15
        $this->logger = new NullLogger();
85
86
        // we may have just rotated the box for packing purposes, record if we did
87 15
        if ($this->box->getInnerWidth() != $this->boxWidth || $this->box->getInnerLength() != $this->boxLength) {
88 3
            $this->boxRotated = true;
89
        }
90 15
    }
91
92
    /**
93
     * Pack as many items as possible into specific given box.
94
     *
95
     * @return PackedBox packed box
96
     */
97 15
    public function pack(): PackedBox
98
    {
99 15
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
100
101 15
        $packedItems = new PackedItemList();
102 15
        $prevItem = null;
103
104 15
        $x = $y = $z = $rowWidth = $rowLength = $layerWidth = $layerLength = $layerDepth = 0;
105
106 15
        $packingWidthLeft = $this->boxWidth;
107 15
        $packingLengthLeft = $this->boxLength;
108 15
        $packingDepthLeft = $this->box->getInnerDepth();
109
110 15
        while (count($this->items) > 0) {
111 15
            $itemToPack = $this->items->extract();
112 15
            $nextItem = $this->getNextItem();
113
114
            //skip items that are simply too heavy or too large
115 15
            if (!$this->checkConstraints($itemToPack, $packedItems)) {
116 3
                $this->rebuildItemList();
117 3
                continue;
118
            }
119
120 15
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $nextItem, $this->hasItemsLeftToPack(), $packingWidthLeft, $packingLengthLeft, $packingDepthLeft);
121
122 15
            if ($orientatedItem instanceof OrientatedItem) {
123 15
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
124 15
                $packedItems->insert($packedItem);
125 15
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
126 15
                $packingWidthLeft -= $orientatedItem->getWidth();
127
128 15
                $rowWidth += $orientatedItem->getWidth();
129 15
                $rowLength = max($rowLength, $orientatedItem->getLength());
130 15
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
131
132
                //allow items to be stacked in place within the same footprint up to current layer depth
133 15
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
134 15
                $this->tryAndStackItemsIntoSpace($packedItems, $prevItem, $nextItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $z + $orientatedItem->getDepth());
135 15
                $x += $orientatedItem->getWidth();
136
137 15
                $prevItem = $packedItem;
138
139 15
                $this->rebuildItemList();
140
            } else {
141 9
                if ($layerWidth == 0 && $layerDepth == 0) { // zero items on layer
142
                    $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
143
                    $prevItem = null;
144
                    continue;
145 9
                } elseif (count($this->items) > 0) { // skip for now, move on to the next item
146 8
                    $this->logger->debug("doesn't fit, skipping for now");
147 8
                    $this->skippedItems->insert($itemToPack);
148 9
                } elseif ($x > 0 && $packingLengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength())) {
149 9
                    $this->logger->debug('No more fit in width wise, resetting for new row');
150 9
                    $layerWidth = max($layerWidth, $rowWidth);
151 9
                    $layerLength += $rowLength;
152 9
                    $packingWidthLeft += $rowWidth;
153 9
                    $packingLengthLeft -= $rowLength;
154 9
                    $y += $rowLength;
155 9
                    $x = $rowWidth = $rowLength = 0;
156 9
                    $this->rebuildItemList($itemToPack);
157 9
                    $prevItem = null;
158 9
                    continue;
159
                } else {
160 7
                    $this->logger->debug('no items fit, so starting next vertical layer');
161
162 7
                    $layerWidth = max($layerWidth, $rowWidth);
163 7
                    $layerLength += $rowLength;
164 7
                    $packingWidthLeft = $rowWidth ? min(intval($layerWidth * 1.1), $this->boxWidth) : $this->boxWidth;
165 7
                    $packingLengthLeft = $rowLength ? min(intval($layerLength * 1.1), $this->boxLength) : $this->boxLength;
166 7
                    $packingDepthLeft -= $layerDepth;
167
168 7
                    $z += $layerDepth;
169 7
                    $x = $y = $rowWidth = $rowLength = $layerWidth = $layerLength = $layerDepth = 0;
170
171 7
                    $this->rebuildItemList($itemToPack);
172 7
                    $prevItem = null;
173
                }
174
            }
175
        }
176 15
        $this->logger->debug('done with this box');
177
178 15
        return $this->createPackedBox($packedItems);
179
    }
180
181
    /**
182
     * @param Item            $itemToPack
183
     * @param PackedItem|null $prevItem
184
     * @param Item|null       $nextItem
185
     * @param bool            $isLastItem
186
     * @param int             $maxWidth
187
     * @param int             $maxLength
188
     * @param int             $maxDepth
189
     *
190
     * @return OrientatedItem|null
191
     */
192 15
    protected function getOrientationForItem(
193
        Item $itemToPack,
194
        ?PackedItem $prevItem,
195
        ?Item $nextItem,
196
        bool $isLastItem,
197
        int $maxWidth,
198
        int $maxLength,
199
        int $maxDepth
200
    ): ?OrientatedItem {
201 15
        $this->logger->debug(
202 15
            "evaluating item {$itemToPack->getDescription()} for fit",
203
            [
204 15
                'item'  => $itemToPack,
205
                'space' => [
206 15
                    'maxWidth'    => $maxWidth,
207 15
                    'maxLength'   => $maxLength,
208 15
                    'maxDepth'    => $maxDepth,
209
                ],
210
            ]
211
        );
212
213 15
        $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
214
215 15
        $orientatedItemFactory = new OrientatedItemFactory();
216 15
        $orientatedItemFactory->setLogger($this->logger);
217 15
        $orientatedItem = $orientatedItemFactory->getBestOrientation($this->box, $itemToPack, $prevOrientatedItem, $nextItem, $isLastItem, $maxWidth, $maxLength, $maxDepth);
218
219 15
        return $orientatedItem;
220
    }
221
222
    /**
223
     * Figure out if we can stack the next item vertically on top of this rather than side by side
224
     * Used when we've packed a tall item, and have just put a shorter one next to it.
225
     *
226
     * @param PackedItemList  $packedItems
227
     * @param PackedItem|null $prevItem
228
     * @param Item|null       $nextItem
229
     * @param int             $maxWidth
230
     * @param int             $maxLength
231
     * @param int             $maxDepth
232
     * @param int             $x
233
     * @param int             $y
234
     * @param int             $z
235
     */
236 15
    protected function tryAndStackItemsIntoSpace(
237
        PackedItemList $packedItems,
238
        ?PackedItem $prevItem,
239
        ?Item $nextItem,
240
        int $maxWidth,
241
        int $maxLength,
242
        int $maxDepth,
243
        int $x,
244
        int $y,
245
        int $z
246
    ): void {
247 15
        while (count($this->items) > 0 && $this->checkNonDimensionalConstraints($this->items->top(), $packedItems)) {
248 13
            $stackedItem = $this->getOrientationForItem(
249 13
                $this->items->top(),
250 13
                $prevItem,
251 13
                $nextItem,
252 13
                $this->items->count() === 1,
253 13
                $maxWidth,
254 13
                $maxLength,
255 13
                $maxDepth
256
            );
257 13
            if ($stackedItem) {
258 1
                $this->remainingWeight -= $this->items->top()->getWeight();
259 1
                $packedItems->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $z));
260 1
                $this->items->extract();
261 1
                $maxDepth -= $stackedItem->getDepth();
262 1
                $z += $stackedItem->getDepth();
263
            } else {
264 13
                break;
265
            }
266
        }
267 15
    }
268
269
    /**
270
     * Check item generally fits into box.
271
     *
272
     * @param Item           $itemToPack
273
     * @param PackedItemList $packedItems
274
     *
275
     * @return bool
276
     */
277 15
    protected function checkConstraints(
278
        Item $itemToPack,
279
        PackedItemList $packedItems
280
    ): bool {
281 15
        return $this->checkNonDimensionalConstraints($itemToPack, $packedItems) &&
282 15
               $this->checkDimensionalConstraints($itemToPack);
283
    }
284
285
    /**
286
     * As well as purely dimensional constraints, there are other constraints that need to be met
287
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
288
     *
289
     * @param Item           $itemToPack
290
     * @param PackedItemList $packedItems
291
     *
292
     * @return bool
293
     */
294 15
    protected function checkNonDimensionalConstraints(Item $itemToPack, PackedItemList $packedItems): bool
295
    {
296 15
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
297
298 15
        if ($itemToPack instanceof ConstrainedItem) {
299 1
            return $weightOK && $itemToPack->canBePackedInBox($packedItems, $this->box);
300
        }
301
302 14
        return $weightOK;
303
    }
304
305
    /**
306
     * Check the item physically fits in the box (at all).
307
     *
308
     * @param Item $itemToPack
309
     *
310
     * @return bool
311
     */
312 15
    protected function checkDimensionalConstraints(Item $itemToPack): bool
313
    {
314 15
        $orientatedItemFactory = new OrientatedItemFactory();
315 15
        $orientatedItemFactory->setLogger($this->logger);
316
317 15
        return (bool) $orientatedItemFactory->getPossibleOrientationsInEmptyBox($itemToPack, $this->box);
318
    }
319
320
    /**
321
     * Reintegrate skipped items into main list.
322
     *
323
     * @param Item|null $currentItem item from current iteration
324
     */
325 15
    protected function rebuildItemList(?Item $currentItem = null): void
326
    {
327 15
        if (count($this->items) === 0) {
328 15
            $this->items = $this->skippedItems;
329 15
            $this->skippedItems = new ItemList();
330
        }
331
332 15
        if ($currentItem instanceof Item) {
333 9
            $this->items->insert($currentItem);
334
        }
335 15
    }
336
337
    /**
338
     * @param PackedItemList $packedItems
339
     *
340
     * @return PackedBox
341
     */
342 15
    protected function createPackedBox(PackedItemList $packedItems): PackedBox
343
    {
344
        //if we rotated the box for packing, need to swap back width/length of the packed items
345 15
        if ($this->boxRotated) {
346 3
            $items = iterator_to_array($packedItems, false);
347 3
            $packedItems = new PackedItemList();
348
            /** @var PackedItem $item */
349 3
            foreach ($items as $item) {
350 3
                $packedItems->insert(
351 3
                    new PackedItem(
352 3
                        $item->getItem(),
353 3
                        $item->getY(),
354 3
                        $item->getX(),
355 3
                        $item->getZ(),
356 3
                        $item->getLength(),
357 3
                        $item->getWidth(),
358 3
                        $item->getDepth()
359
                    )
360
                );
361
            }
362
        }
363
364 15
        return new PackedBox($this->box, $packedItems);
365
    }
366
367
    /**
368
     * Are there items left to pack?
369
     *
370
     * @return bool
371
     */
372 15
    protected function hasItemsLeftToPack(): bool
373
    {
374 15
        return count($this->skippedItems) + count($this->items) === 0;
375
    }
376
377
    /**
378
     * Return next item in line for packing.
379
     *
380
     * @return Item|null
381
     */
382 15
    protected function getNextItem(): ?Item
383
    {
384 15
        return count($this->items) ? $this->items->top() : null;
385
    }
386
}
387