Passed
Push — 1.x-dev ( b2df86...afef98 )
by Doug
03:17
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 9
            $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 9
            $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
138
            //skip items that are simply too heavy or too large
139 28
            if (!$this->checkConstraints($itemToPack)) {
140 6
                $this->rebuildItemList();
141 6
                continue;
142
            }
143
144 28
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, $this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft);
145
146 28
            if ($orientatedItem instanceof OrientatedItem) {
147 28
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth);
148 28
                $layer->insert($packedItem);
149 28
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
150 28
                $widthLeft -= $orientatedItem->getWidth();
151
152 28
                $rowWidth += $orientatedItem->getWidth();
153 28
                $rowLength = max($rowLength, $orientatedItem->getLength());
154 28
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
155
156
                //allow items to be stacked in place within the same footprint up to current layer depth
157 28
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
158 28
                $this->tryAndStackItemsIntoSpace($layer, $prevItem, $this->items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth());
159 28
                $x += $orientatedItem->getWidth();
160
161 28
                $prevItem = $packedItem;
162 28
                $this->rebuildItemList();
163
            } else {
164 20
                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 20
                } 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 20
                } elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength(), $itemToPack->getDepth())) {
171 20
                    $this->logger->debug('No more fit in width wise, resetting for new row');
172 20
                    $widthLeft += $rowWidth;
173 20
                    $lengthLeft -= $rowLength;
174 20
                    $y += $rowLength;
175 20
                    $x = $rowWidth = $rowLength = 0;
176 20
                    $this->rebuildItemList($itemToPack);
177 20
                    $prevItem = null;
178 20
                    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 28
    }
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 28
    public function stabiliseLayers()
196
    {
197 28
        $stabiliser = new LayerStabiliser();
198 28
        $this->layers = $stabiliser->stabilise($this->layers);
199 28
    }
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 28
    protected function getOrientationForItem(
213
        Item $itemToPack,
214
        PackedItem $prevItem = null,
215
        ItemList $nextItems,
216
        $isLastItem,
217
        $maxWidth,
218
        $maxLength,
219
        $maxDepth
220
    ) {
221 28
        $this->logger->debug(
222 28
            "evaluating item {$itemToPack->getDescription()} for fit",
223
            [
224 28
                'item' => $itemToPack,
225
                'space' => [
226 28
                    'maxWidth' => $maxWidth,
227 28
                    'maxLength' => $maxLength,
228 28
                    'maxDepth' => $maxDepth,
229
                ],
230
            ]
231
        );
232
233 28
        $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
234
235 28
        $orientatedItemFactory = new OrientatedItemFactory($this->box);
236 28
        $orientatedItemFactory->setLogger($this->logger);
237 28
        $orientatedItemDecision = $orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth);
238
239 28
        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 28
    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 28
        while (count($this->items) > 0 && $this->checkNonDimensionalConstraints($this->items->top())) {
268 28
            $stackedItem = $this->getOrientationForItem(
269 28
                $this->items->top(),
270 28
                $prevItem,
271 28
                $nextItems,
272 28
                $this->items->count() === 1,
273 28
                $maxWidth,
274 28
                $maxLength,
275 28
                $maxDepth
276
            );
277 28
            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 28
                break;
285
            }
286
        }
287 28
    }
288
289
    /**
290
     * Check item generally fits into box.
291
     *
292
     * @param Item $itemToPack
293
     *
294
     * @return bool
295
     */
296 28
    protected function checkConstraints(
297
        Item $itemToPack
298
    ) {
299 28
        return $this->checkNonDimensionalConstraints($itemToPack) &&
300 28
               $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 28
    protected function checkNonDimensionalConstraints(Item $itemToPack)
312
    {
313 28
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
314
315 28
        if ($itemToPack instanceof ConstrainedItem) {
316 1
            return $weightOK && $itemToPack->canBePackedInBox($this->getPackedItemList()->asItemList(), $this->box);
317
        }
318
319 28
        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 28
    protected function checkDimensionalConstraints(Item $itemToPack)
330
    {
331 28
        $orientatedItemFactory = new OrientatedItemFactory($this->box);
332 28
        $orientatedItemFactory->setLogger($this->logger);
333
334 28
        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 28
    protected function rebuildItemList(Item $currentItem = null)
343
    {
344 28
        if (count($this->items) === 0) {
345 28
            $this->items = $this->skippedItems;
346 28
            $this->skippedItems = new ItemList();
347
        }
348
349 28
        if ($currentItem instanceof Item) {
350 20
            $this->items->insert($currentItem);
351
        }
352 28
    }
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 28
    protected function hasItemsLeftToPack()
375
    {
376 28
        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 28
    protected function getPackedItemList()
385
    {
386 28
        $packedItemList = new PackedItemList();
387 28
        foreach ($this->layers as $layer) {
388 28
            foreach ($layer->getItems() as $packedItem) {
389 28
                $packedItemList->insert($packedItem);
390
            }
391
        }
392
393 28
        return $packedItemList;
394
    }
395
396
    /**
397
     * Return the current packed depth.
398
     *
399
     * @return int
400
     */
401 28
    protected function getCurrentPackedDepth()
402
    {
403 28
        $depth = 0;
404 28
        foreach ($this->layers as $layer) {
405 13
            $depth += $layer->getDepth();
406
        }
407
408 28
        return $depth;
409
    }
410
}
411