Passed
Push — 1.x-dev ( cc1b76...09423a )
by Doug
02:10
created

VolumePacker::tryAndStackItemsIntoSpace()   B

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 8.5806
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 26
    public function __construct(Box $box, ItemList $items)
80
    {
81 26
        $this->box = $box;
82 26
        $this->items = $items;
83
84 26
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
85 26
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
86 26
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
87 26
        $this->skippedItems = new ItemList();
88 26
        $this->logger = new NullLogger();
89
90
        // we may have just rotated the box for packing purposes, record if we did
91 26
        if ($this->box->getInnerWidth() != $this->boxWidth || $this->box->getInnerLength() != $this->boxLength) {
92 7
            $this->boxRotated = true;
93
        }
94 26
    }
95
96
    /**
97
     * Pack as many items as possible into specific given box.
98
     *
99
     * @return PackedBox packed box
100
     */
101 26
    public function pack()
102
    {
103 26
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
104
105 26
        while (count($this->items) > 0) {
106 26
            $layerStartDepth = $this->getCurrentPackedDepth();
107 26
            $this->packLayer($layerStartDepth, $this->boxWidth, $this->boxLength, $this->box->getInnerDepth() - $layerStartDepth);
108
        }
109
110 26
        if ($this->boxRotated) {
111 7
            $this->rotateLayersNinetyDegrees();
112
        }
113
114 26
        $this->stabiliseLayers();
115
116 26
        $this->logger->debug('done with this box');
117
118 26
        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 26
    protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft)
130
    {
131 26
        $this->layers[] = $layer = new PackedLayer();
132 26
        $prevItem = null;
133 26
        $x = $y = $rowWidth = $rowLength = $layerDepth = 0;
134
135 26
        while (count($this->items) > 0) {
136 26
            $itemToPack = $this->items->extract();
137 26
            $nextItem = $this->getNextItem();
138
139
            //skip items that are simply too heavy or too large
140 26
            if (!$this->checkConstraints($itemToPack)) {
141 6
                $this->rebuildItemList();
142 6
                continue;
143
            }
144
145 26
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $nextItem, $this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft);
146
147 26
            if ($orientatedItem instanceof OrientatedItem) {
148 26
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth);
149 26
                $layer->insert($packedItem);
150 26
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
151 26
                $widthLeft -= $orientatedItem->getWidth();
152
153 26
                $rowWidth += $orientatedItem->getWidth();
154 26
                $rowLength = max($rowLength, $orientatedItem->getLength());
155 26
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
156
157
                //allow items to be stacked in place within the same footprint up to current layer depth
158 26
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
159 26
                $this->tryAndStackItemsIntoSpace($layer, $prevItem, $nextItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth());
160 26
                $x += $orientatedItem->getWidth();
161
162 26
                $prevItem = $packedItem;
163 26
                $this->rebuildItemList();
164
            } else {
165 20
                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 20
                } elseif (count($this->items) > 0) { // skip for now, move on to the next item
169 18
                    $this->logger->debug("doesn't fit, skipping for now");
170 18
                    $this->skippedItems->insert($itemToPack);
171 20
                } elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength())) {
172 20
                    $this->logger->debug('No more fit in width wise, resetting for new row');
173 20
                    $widthLeft += $rowWidth;
174 20
                    $lengthLeft -= $rowLength;
175 20
                    $y += $rowLength;
176 20
                    $x = $rowWidth = $rowLength = 0;
177 20
                    $this->rebuildItemList($itemToPack);
178 20
                    $prevItem = null;
179 20
                    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 26
    }
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 26
    public function stabiliseLayers()
197
    {
198 26
        $stabiliser = new LayerStabiliser();
199 26
        $this->layers = $stabiliser->stabilise($this->layers);
200 26
    }
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 26
    protected function getOrientationForItem(
214
        Item $itemToPack,
215
        PackedItem $prevItem = null,
216
        Item $nextItem = null,
217
        $isLastItem,
218
        $maxWidth,
219
        $maxLength,
220
        $maxDepth
221
    ) {
222 26
        $this->logger->debug(
223 26
            "evaluating item {$itemToPack->getDescription()} for fit",
224
            [
225 26
                'item'  => $itemToPack,
226
                'space' => [
227 26
                    'maxWidth'    => $maxWidth,
228 26
                    'maxLength'   => $maxLength,
229 26
                    'maxDepth'    => $maxDepth,
230
                ],
231
            ]
232
        );
233
234 26
        $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
235
236 26
        $orientatedItemFactory = new OrientatedItemFactory($itemToPack, $this->box);
237 26
        $orientatedItemFactory->setLogger($this->logger);
238 26
        $orientatedItem = $orientatedItemFactory->getBestOrientation($prevOrientatedItem, $nextItem, $isLastItem, $maxWidth, $maxLength, $maxDepth);
239
240 26
        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 26
    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 26
        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 26
    }
289
290
    /**
291
     * Check item generally fits into box.
292
     *
293
     * @param Item $itemToPack
294
     *
295
     * @return bool
296
     */
297 26
    protected function checkConstraints(
298
        Item $itemToPack
299
    ) {
300 26
        return $this->checkNonDimensionalConstraints($itemToPack) &&
301 26
               $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 26
    protected function checkNonDimensionalConstraints(Item $itemToPack)
313
    {
314 26
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
315
316 26
        if ($itemToPack instanceof ConstrainedItem) {
317 1
            return $weightOK && $itemToPack->canBePackedInBox($this->getPackedItemList()->asItemList(), $this->box);
318
        }
319
320 26
        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 26
    protected function checkDimensionalConstraints(Item $itemToPack)
331
    {
332 26
        $orientatedItemFactory = new OrientatedItemFactory($itemToPack, $this->box);
333 26
        $orientatedItemFactory->setLogger($this->logger);
334
335 26
        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 26
    protected function rebuildItemList(Item $currentItem = null)
344
    {
345 26
        if (count($this->items) === 0) {
346 26
            $this->items = $this->skippedItems;
347 26
            $this->skippedItems = new ItemList();
348
        }
349
350 26
        if ($currentItem instanceof Item) {
351 20
            $this->items->insert($currentItem);
352
        }
353 26
    }
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()
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 26
    protected function hasItemsLeftToPack()
376
    {
377 26
        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 26
    protected function getNextItem()
386
    {
387 26
        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 26
    protected function getPackedItemList()
396
    {
397 26
        $packedItemList = new PackedItemList();
398 26
        foreach ($this->layers as $layer) {
399 26
            foreach ($layer->getItems() as $packedItem) {
400 26
                $packedItemList->insert($packedItem);
401
            }
402
        }
403
404 26
        return $packedItemList;
405
    }
406
407
    /**
408
     * Return the current packed depth.
409
     *
410
     * @return int
411
     */
412 26
    protected function getCurrentPackedDepth()
413
    {
414 26
        $depth = 0;
415 26
        foreach ($this->layers as $layer) {
416 13
            $depth += $layer->getDepth();
417
        }
418
419 26
        return $depth;
420
    }
421
}
422