Passed
Push — 2.x-dev ( e5166d...a75e6c )
by Doug
02:18
created

VolumePacker::tryAndStackItemsIntoSpace()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 3
nop 9
dl 0
loc 29
ccs 17
cts 17
cp 1
crap 4
rs 9.7
c 0
b 0
f 0

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 temporarily skipped 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
     * @var PackedLayer[]
70
     */
71
    protected $layers = [];
72
73
    /**
74
     * Constructor.
75
     *
76
     * @param Box      $box
77
     * @param ItemList $items
78
     */
79 28
    public function __construct(Box $box, ItemList $items)
80
    {
81 28
        $this->box = $box;
82 28
        $this->items = $items;
83
84 28
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
85 28
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
86 28
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
87 28
        $this->skippedItems = new ItemList();
88 28
        $this->logger = new NullLogger();
89
90
        // we may have just rotated the box for packing purposes, record if we did
91 28
        if ($this->box->getInnerWidth() != $this->boxWidth || $this->box->getInnerLength() != $this->boxLength) {
92 8
            $this->boxRotated = true;
93
        }
94 28
    }
95
96
    /**
97
     * Pack as many items as possible into specific given box.
98
     *
99
     * @return PackedBox packed box
100
     */
101 28
    public function pack()
102
    {
103 28
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
104
105 28
        while (count($this->items) > 0) {
106 28
            $layerStartDepth = $this->getCurrentPackedDepth();
107 28
            $this->packLayer($layerStartDepth, $this->boxWidth, $this->boxLength, $this->box->getInnerDepth() - $layerStartDepth);
108
        }
109
110 28
        if ($this->boxRotated) {
111 8
            $this->rotateLayersNinetyDegrees();
112
        }
113
114 28
        $this->stabiliseLayers();
115
116 28
        $this->logger->debug('done with this box');
117
118 28
        return PackedBox::fromPackedItemList($this->box, $this->getPackedItemList());
119
    }
120
121
    /**
122
     * Pack items into an individual vertical layer.
123
     *
124
     * @param int $startDepth
125
     * @param int $widthLeft
126
     * @param int $lengthLeft
127
     * @param int $depthLeft
128
     */
129 28
    protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft)
130
    {
131 28
        $this->layers[] = $layer = new PackedLayer();
132 28
        $prevItem = null;
133 28
        $x = $y = $rowWidth = $rowLength = $layerDepth = 0;
134
135 28
        while (count($this->items) > 0) {
136 28
            $itemToPack = $this->items->extract();
137 28
            $nextItem = $this->getNextItem();
138
139
            //skip items that are simply too heavy or too large
140 28
            if (!$this->checkConstraints($itemToPack)) {
141 6
                $this->rebuildItemList();
142 6
                continue;
143
            }
144
145 28
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $nextItem, $this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft);
146
147 28
            if ($orientatedItem instanceof OrientatedItem) {
148 28
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth);
149 28
                $layer->insert($packedItem);
150 28
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
151 28
                $widthLeft -= $orientatedItem->getWidth();
152
153 28
                $rowWidth += $orientatedItem->getWidth();
154 28
                $rowLength = max($rowLength, $orientatedItem->getLength());
155 28
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
156
157
                //allow items to be stacked in place within the same footprint up to current layer depth
158 28
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
159 28
                $this->tryAndStackItemsIntoSpace($layer, $prevItem, $nextItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth());
160 28
                $x += $orientatedItem->getWidth();
161
162 28
                $prevItem = $packedItem;
163 28
                $this->rebuildItemList();
164
            } else {
165 21
                if (count($layer->getItems()) === 0) { // zero items on layer
166 1
                    $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
167 1
                    continue;
168 21
                } elseif (count($this->items) > 0) { // skip for now, move on to the next item
169 19
                    $this->logger->debug("doesn't fit, skipping for now");
170 19
                    $this->skippedItems->insert($itemToPack);
171 21
                } elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength())) {
172 21
                    $this->logger->debug('No more fit in width wise, resetting for new row');
173 21
                    $widthLeft += $rowWidth;
174 21
                    $lengthLeft -= $rowLength;
175 21
                    $y += $rowLength;
176 21
                    $x = $rowWidth = $rowLength = 0;
177 21
                    $this->rebuildItemList($itemToPack);
178 21
                    $prevItem = null;
179 21
                    continue;
180
                } else {
181 13
                    $this->logger->debug('no items fit, so starting next vertical layer');
182 13
                    $this->rebuildItemList($itemToPack);
183
184 13
                    return;
185
                }
186
            }
187
        }
188 28
    }
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 28
    public function stabiliseLayers()
197
    {
198 28
        $stabiliser = new LayerStabiliser();
199 28
        $this->layers = $stabiliser->stabilise($this->layers);
200 28
    }
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 28
    protected function getOrientationForItem(
214
        Item $itemToPack,
215
        PackedItem $prevItem = null,
216
        Item $nextItem = null,
217
        $isLastItem,
218
        $maxWidth,
219
        $maxLength,
220
        $maxDepth
221
    ) {
222 28
        $this->logger->debug(
223 28
            "evaluating item {$itemToPack->getDescription()} for fit",
224
            [
225 28
                'item'  => $itemToPack,
226
                'space' => [
227 28
                    'maxWidth'    => $maxWidth,
228 28
                    'maxLength'   => $maxLength,
229 28
                    'maxDepth'    => $maxDepth,
230
                ],
231
            ]
232
        );
233
234 28
        $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
235
236 28
        $orientatedItemFactory = new OrientatedItemFactory($itemToPack, $this->box);
237 28
        $orientatedItemFactory->setLogger($this->logger);
238 28
        $orientatedItem = $orientatedItemFactory->getBestOrientation($prevOrientatedItem, $nextItem, $isLastItem, $maxWidth, $maxLength, $maxDepth);
239
240 28
        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 28
    protected function tryAndStackItemsIntoSpace(
258
        PackedLayer $layer,
259
        PackedItem $prevItem = null,
260
        Item $nextItem = null,
261
        $maxWidth,
262
        $maxLength,
263
        $maxDepth,
264
        $x,
265
        $y,
266
        $z
267
    ) {
268 28
        while (count($this->items) > 0 && $this->checkNonDimensionalConstraints($this->items->top())) {
269 27
            $stackedItem = $this->getOrientationForItem(
270 27
                $this->items->top(),
271 27
                $prevItem,
272 27
                $nextItem,
273 27
                $this->items->count() === 1,
274 27
                $maxWidth,
275 27
                $maxLength,
276 27
                $maxDepth
277
            );
278 27
            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 27
                break;
286
            }
287
        }
288 28
    }
289
290
    /**
291
     * Check item generally fits into box.
292
     *
293
     * @param Item $itemToPack
294
     *
295
     * @return bool
296
     */
297 28
    protected function checkConstraints(
298
        Item $itemToPack
299
    ) {
300 28
        return $this->checkNonDimensionalConstraints($itemToPack) &&
301 28
               $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 28
    protected function checkNonDimensionalConstraints(Item $itemToPack)
313
    {
314 28
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
315
316 28
        if ($itemToPack instanceof ConstrainedItem) {
317 1
            return $weightOK && $itemToPack->canBePackedInBox($this->getPackedItemList()->asItemList(), $this->box);
318
        }
319
320 28
        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 28
    protected function checkDimensionalConstraints(Item $itemToPack)
331
    {
332 28
        $orientatedItemFactory = new OrientatedItemFactory($itemToPack, $this->box);
333 28
        $orientatedItemFactory->setLogger($this->logger);
334
335 28
        return (bool) $orientatedItemFactory->getPossibleOrientationsInEmptyBox();
336
    }
337
338
    /**
339
     * Reintegrate skipped items into main list.
340
     *
341
     * @param Item|null $currentItem item from current iteration
342
     */
343 28
    protected function rebuildItemList(Item $currentItem = null)
344
    {
345 28
        if (count($this->items) === 0) {
346 28
            $this->items = $this->skippedItems;
347 28
            $this->skippedItems = new ItemList();
348
        }
349
350 28
        if ($currentItem instanceof Item) {
351 21
            $this->items->insert($currentItem);
352
        }
353 28
    }
354
355
    /**
356
     * Swap back width/length of the packed items to match orientation of the box if needed.
357
     */
358 8
    protected function rotateLayersNinetyDegrees()
359
    {
360 8
        foreach ($this->layers as $i => $originalLayer) {
361 8
            $newLayer = new PackedLayer();
362 8
            foreach ($originalLayer->getItems() as $item) {
363 8
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
364 8
                $newLayer->insert($packedItem);
365
            }
366 8
            $this->layers[$i] = $newLayer;
367
        }
368 8
    }
369
370
    /**
371
     * Are there items left to pack?
372
     *
373
     * @return bool
374
     */
375 28
    protected function hasItemsLeftToPack()
376
    {
377 28
        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 28
    protected function getNextItem()
386
    {
387 28
        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 28
    protected function getPackedItemList()
396
    {
397 28
        $packedItemList = new PackedItemList();
398 28
        foreach ($this->layers as $layer) {
399 28
            foreach ($layer->getItems() as $packedItem) {
400 28
                $packedItemList->insert($packedItem);
401
            }
402
        }
403
404 28
        return $packedItemList;
405
    }
406
407
    /**
408
     * Return the current packed depth.
409
     *
410
     * @return int
411
     */
412 28
    protected function getCurrentPackedDepth()
413
    {
414 28
        $depth = 0;
415 28
        foreach ($this->layers as $layer) {
416 13
            $depth += $layer->getDepth();
417
        }
418
419 28
        return $depth;
420
    }
421
}
422