Passed
Push — 1.x-dev ( 9106e4...cc1b76 )
by Doug
02:13
created

VolumePacker::tryAndStackItemsIntoSpace()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 32
Code Lines 27

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