Completed
Push — 1.x-dev ( f03c49...17b59f )
by Doug
68:27 queued 64:38
created

VolumePacker::tryAndStackItemsIntoSpace()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 21
nc 3
nop 10
dl 0
loc 34
ccs 12
cts 12
cp 1
crap 4
rs 9.584
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
namespace DVDoug\BoxPacker;
8
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerInterface;
11
use Psr\Log\NullLogger;
12
13
/**
14
 * Actual packer.
15
 *
16
 * @author Doug Wright
17
 */
18
class VolumePacker implements LoggerAwareInterface
19
{
20
    /**
21
     * The logger instance.
22
     *
23
     * @var LoggerInterface
24
     */
25
    protected $logger;
26
27
    /**
28
     * Box to pack items into.
29
     *
30
     * @var Box
31
     */
32
    protected $box;
33
34
    /**
35
     * @var int
36
     */
37
    protected $boxWidth;
38
39
    /**
40
     * @var int
41
     */
42
    protected $boxLength;
43
44
    /**
45
     * List of items to be packed.
46
     *
47
     * @var ItemList
48
     */
49
    protected $items;
50
51
    /**
52
     * List of items temporarily skipped to be packed.
53
     *
54
     * @var array
55
     */
56
    protected $skippedItems = [];
57
58
    /**
59
     * Remaining weight capacity of the box.
60
     *
61
     * @var int
62
     */
63
    protected $remainingWeight;
64
65
    /**
66
     * Whether the box was rotated for packing.
67
     *
68
     * @var bool
69
     */
70
    protected $boxRotated = false;
71
72
    /**
73
     * @var PackedLayer[]
74
     */
75
    protected $layers = [];
76
77
    /**
78
     * Whether the packer is in look-ahead mode (i.e. working ahead of the main packing).
79
     *
80
     * @var bool
81
     */
82
    protected $lookAheadMode = false;
83
84
    /**
85
     * @var OrientatedItemFactory
86
     */
87
    private $orientatedItemFactory;
88
89
    /**
90
     * Constructor.
91
     *
92
     * @param Box      $box
93
     * @param ItemList $items
94
     */
95 32
    public function __construct(Box $box, ItemList $items)
96
    {
97 32
        $this->box = $box;
98 32
        $this->items = $items;
99
100 32
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
101 32
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
102 32
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
103 32
        $this->logger = new NullLogger();
104
105
        // we may have just rotated the box for packing purposes, record if we did
106 32
        if ($this->box->getInnerWidth() !== $this->boxWidth || $this->box->getInnerLength() !== $this->boxLength) {
107 7
            $this->boxRotated = true;
108
        }
109
110 32
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
111 32
    }
112
113
    /**
114
     * Sets a logger.
115
     *
116
     * @param LoggerInterface $logger
117
     */
118 17
    public function setLogger(LoggerInterface $logger)
119
    {
120 17
        $this->logger = $logger;
121 17
        $this->orientatedItemFactory->setLogger($logger);
122 17
    }
123
124
    /**
125
     * @internal
126
     * @param bool $lookAhead
127
     */
128
    public function setLookAheadMode($lookAhead)
129
    {
130
        $this->lookAheadMode = $lookAhead;
131
    }
132
133
    /**
134
     * Pack as many items as possible into specific given box.
135
     *
136
     * @return PackedBox packed box
137
     */
138 32
    public function pack()
139
    {
140 32
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);
141
142 32
        while ($this->items->count() > 0) {
143 32
            $layerStartDepth = $this->getCurrentPackedDepth();
144 32
            $this->packLayer($layerStartDepth, $this->boxWidth, $this->boxLength, $this->box->getInnerDepth() - $layerStartDepth);
145
        }
146
147 30
        if ($this->boxRotated) {
148 7
            $this->rotateLayersNinetyDegrees();
149
        }
150
151 30
        if (!$this->lookAheadMode) {
152 30
            $this->stabiliseLayers();
153
        }
154
155 30
        $this->logger->debug('done with this box');
156
157 30
        return PackedBox::fromPackedItemList($this->box, $this->getPackedItemList());
158
    }
159
160
    /**
161
     * Pack items into an individual vertical layer.
162
     *
163
     * @param int $startDepth
164
     * @param int $widthLeft
165
     * @param int $lengthLeft
166
     * @param int $depthLeft
167
     */
168 32
    protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft)
169
    {
170 32
        $this->layers[] = $layer = new PackedLayer();
171 32
        $prevItem = null;
172 32
        $x = $y = $rowWidth = $rowLength = $layerDepth = 0;
173
174 32
        while ($this->items->count() > 0) {
175 32
            $itemToPack = $this->items->extract();
176
177
            //skip items that are simply too heavy or too large
178 32
            if (!$this->checkNonDimensionalConstraints($itemToPack)) {
179 4
                continue;
180
            }
181
182 32
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, !$this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $startDepth);
183
184 30
            if ($orientatedItem instanceof OrientatedItem) {
185 30
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth);
186 30
                $layer->insert($packedItem);
187 30
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
188 30
                $widthLeft -= $orientatedItem->getWidth();
189
190 30
                $rowWidth += $orientatedItem->getWidth();
191 30
                $rowLength = max($rowLength, $orientatedItem->getLength());
192 30
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
193
194
                //allow items to be stacked in place within the same footprint up to current layer depth
195 30
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
196 30
                $this->tryAndStackItemsIntoSpace($layer, $prevItem, $this->items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth(), $rowLength);
197 30
                $x += $orientatedItem->getWidth();
198
199 30
                $prevItem = $packedItem;
200 30
                if ($this->items->count() === 0) {
201 30
                    $this->rebuildItemList();
202
                }
203 25
            } elseif (count($layer->getItems()) === 0) { // zero items on layer
204 7
                $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
205 7
                continue;
206 24
            } elseif ($widthLeft > 0 && $this->items->count() > 0) { // skip for now, move on to the next item
207 21
                $this->logger->debug("doesn't fit, skipping for now");
208 21
                $this->skippedItems[] = $itemToPack;
209
                // abandon here if next item is the same, no point trying to keep going. Last time is not skipped, need that to trigger appropriate reset logic
210 21
                while ($this->items->count() > 2 && $this->orientatedItemFactory->isSameDimensions($itemToPack, $this->items->top())) {
211 9
                    $this->skippedItems[] = $this->items->extract();
212
                }
213 24
            } elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength(), $itemToPack->getDepth())) {
214 24
                $this->logger->debug('No more fit in width wise, resetting for new row');
215 24
                $widthLeft += $rowWidth;
216 24
                $lengthLeft -= $rowLength;
217 24
                $y += $rowLength;
218 24
                $x = $rowWidth = $rowLength = 0;
219 24
                $this->skippedItems[] = $itemToPack;
220 24
                $this->rebuildItemList();
221 24
                $prevItem = null;
222 24
                continue;
223
            } else {
224 22
                $this->logger->debug('no items fit, so starting next vertical layer');
225 22
                $this->skippedItems[] = $itemToPack;
226 22
                $this->rebuildItemList();
227
228 22
                return;
229
            }
230
        }
231 30
    }
232
233
    /**
234
     * During packing, it is quite possible that layers have been created that aren't physically stable
235
     * i.e. they overhang the ones below.
236
     *
237
     * This function reorders them so that the ones with the greatest surface area are placed at the bottom
238
     */
239 30
    public function stabiliseLayers()
240
    {
241 30
        $stabiliser = new LayerStabiliser();
242 30
        $this->layers = $stabiliser->stabilise($this->layers);
243 30
    }
244
245
    /**
246
     * @param Item            $itemToPack
247
     * @param PackedItem|null $prevItem
248
     * @param ItemList        $nextItems
249
     * @param bool            $isLastItem
250
     * @param int             $maxWidth
251
     * @param int             $maxLength
252
     * @param int             $maxDepth
253
     * @param int             $rowLength
254
     * @param int             $x
255
     * @param int             $y
256
     * @param int             $z
257
     *
258
     * @return OrientatedItem|null
259
     */
260 32
    protected function getOrientationForItem(
261
        Item $itemToPack,
262
        PackedItem $prevItem = null,
263
        ItemList $nextItems,
264
        $isLastItem,
265
        $maxWidth,
266
        $maxLength,
267
        $maxDepth,
268
        $rowLength,
269
        $x,
270
        $y,
271
        $z
272
    ) {
273 32
        $this->logger->debug(
274 32
            "evaluating item {$itemToPack->getDescription()} for fit",
275
            [
276 32
                'item' => $itemToPack,
277
                'space' => [
278 32
                    'maxWidth' => $maxWidth,
279 32
                    'maxLength' => $maxLength,
280 32
                    'maxDepth' => $maxDepth,
281
                ],
282
            ]
283
        );
284
285 32
        $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
286 32
        $prevPackedItemList = $itemToPack instanceof ConstrainedPlacementItem ? $this->getPackedItemList() : new PackedItemList(); // don't calculate it if not going to be used
287
288 32
        $orientatedItemDecision = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth, $rowLength, $x, $y, $z, $prevPackedItemList);
289
290 30
        return $orientatedItemDecision;
291
    }
292
293
    /**
294
     * Figure out if we can stack the next item vertically on top of this rather than side by side
295
     * Used when we've packed a tall item, and have just put a shorter one next to it.
296
     *
297
     * @param PackedLayer     $layer
298
     * @param PackedItem|null $prevItem
299
     * @param ItemList        $nextItems
300
     * @param int             $maxWidth
301
     * @param int             $maxLength
302
     * @param int             $maxDepth
303
     * @param int             $x
304
     * @param int             $y
305
     * @param int             $z
306
     */
307 30
    protected function tryAndStackItemsIntoSpace(
308
        PackedLayer $layer,
309
        PackedItem $prevItem = null,
310
        ItemList $nextItems,
311
        $maxWidth,
312
        $maxLength,
313
        $maxDepth,
314
        $x,
315
        $y,
316
        $z,
317
        $rowLength
318
    ) {
319 30
        while ($this->items->count() > 0 && $this->checkNonDimensionalConstraints($this->items->top())) {
320 29
            $stackedItem = $this->getOrientationForItem(
321 29
                $this->items->top(),
322
                $prevItem,
323
                $nextItems,
324 29
                $this->items->count() === 1,
325
                $maxWidth,
326
                $maxLength,
327
                $maxDepth,
328
                $rowLength,
329
                $x,
330
                $y,
331
                $z
332
            );
333 29
            if ($stackedItem) {
334 4
                $this->remainingWeight -= $this->items->top()->getWeight();
335 4
                $layer->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $z));
336 4
                $this->items->extract();
337 4
                $maxDepth -= $stackedItem->getDepth();
338 4
                $z += $stackedItem->getDepth();
339
            } else {
340 29
                break;
341
            }
342
        }
343 30
    }
344
345
    /**
346
     * As well as purely dimensional constraints, there are other constraints that need to be met
347
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
348
     *
349
     * @param Item $itemToPack
350
     *
351
     * @return bool
352
     */
353 32
    protected function checkNonDimensionalConstraints(Item $itemToPack)
354
    {
355 32
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
356
357 32
        if ($itemToPack instanceof ConstrainedItem) {
358 1
            return $weightOK && $itemToPack->canBePackedInBox($this->getPackedItemList()->asItemList(), $this->box);
359
        }
360
361 32
        return $weightOK;
362
    }
363
364
    /**
365
     * Check the item physically fits in the box (at all).
366
     *
367
     * @param Item $itemToPack
368
     *
369
     * @return bool
370
     */
371
    protected function checkDimensionalConstraints(Item $itemToPack)
372
    {
373
        $orientatedItemFactory = new OrientatedItemFactory($this->box);
374
        $orientatedItemFactory->setLogger($this->logger);
375
376
        return (bool) $orientatedItemFactory->getPossibleOrientationsInEmptyBox($itemToPack);
377
    }
378
379
    /**
380
     * Reintegrate skipped items into main list.
381
     */
382 30
    protected function rebuildItemList()
383
    {
384 30
        while(count($this->skippedItems)) {
385 24
            $this->items->insert(array_pop($this->skippedItems));
386
        }
387 30
    }
388
389
    /**
390
     * Swap back width/length of the packed items to match orientation of the box if needed.
391
     */
392 7
    protected function rotateLayersNinetyDegrees()
393
    {
394 7
        foreach ($this->layers as $i => $originalLayer) {
395 7
            $newLayer = new PackedLayer();
396 7
            foreach ($originalLayer->getItems() as $item) {
397 7
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
398 7
                $newLayer->insert($packedItem);
399
            }
400 7
            $this->layers[$i] = $newLayer;
401
        }
402 7
    }
403
404
    /**
405
     * Are there items left to pack?
406
     *
407
     * @return bool
408
     */
409 32
    protected function hasItemsLeftToPack()
410
    {
411 32
        return count($this->skippedItems) + $this->items->count() > 0;
412
    }
413
414
    /**
415
     * Generate a single list of items packed.
416
     *
417
     * @return PackedItemList
418
     */
419 30
    protected function getPackedItemList()
420
    {
421 30
        $packedItemList = new PackedItemList();
422 30
        foreach ($this->layers as $layer) {
423 30
            foreach ($layer->getItems() as $packedItem) {
424 30
                $packedItemList->insert($packedItem);
425
            }
426
        }
427
428 30
        return $packedItemList;
429
    }
430
431
    /**
432
     * Return the current packed depth.
433
     *
434
     * @return int
435
     */
436 32
    protected function getCurrentPackedDepth()
437
    {
438 32
        $depth = 0;
439 32
        foreach ($this->layers as $layer) {
440 22
            $depth += $layer->getDepth();
441
        }
442
443 32
        return $depth;
444
    }
445
}
446