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