Passed
Push — 1.x ( 0e8de8...457fa0 )
by Doug
30:53
created

VolumePacker::tryAndStackItemsIntoSpace()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 21
c 0
b 0
f 0
nc 3
nop 10
dl 0
loc 34
rs 9.584

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
    public function __construct(Box $box, ItemList $items)
96
    {
97
        $this->box = $box;
98
        $this->items = $items;
99
100
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
101
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
102
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
103
        $this->logger = new NullLogger();
104
105
        // we may have just rotated the box for packing purposes, record if we did
106
        if ($this->box->getInnerWidth() !== $this->boxWidth || $this->box->getInnerLength() !== $this->boxLength) {
107
            $this->boxRotated = true;
108
        }
109
110
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
111
    }
112
113
    /**
114
     * Sets a logger.
115
     *
116
     * @param LoggerInterface $logger
117
     */
118
    public function setLogger(LoggerInterface $logger)
119
    {
120
        $this->logger = $logger;
121
        $this->orientatedItemFactory->setLogger($logger);
122
    }
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
    public function pack()
139
    {
140
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);
141
142
        while ($this->items->count() > 0) {
143
            $layerStartDepth = $this->getCurrentPackedDepth();
144
            $this->packLayer($layerStartDepth, $this->boxWidth, $this->boxLength, $this->box->getInnerDepth() - $layerStartDepth);
145
        }
146
147
        if ($this->boxRotated) {
148
            $this->rotateLayersNinetyDegrees();
149
        }
150
151
        if (!$this->lookAheadMode) {
152
            $this->stabiliseLayers();
153
        }
154
155
        $this->logger->debug('done with this box');
156
157
        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
    protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft)
169
    {
170
        $this->layers[] = $layer = new PackedLayer();
171
        $prevItem = null;
172
        $x = $y = $rowWidth = $rowLength = $layerDepth = 0;
173
174
        while ($this->items->count() > 0) {
175
            $itemToPack = $this->items->extract();
176
177
            //skip items that are simply too heavy or too large
178
            if (!$this->checkNonDimensionalConstraints($itemToPack)) {
179
                continue;
180
            }
181
182
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, !$this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $startDepth);
183
184
            if ($orientatedItem instanceof OrientatedItem) {
185
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth);
186
                $layer->insert($packedItem);
187
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
188
                $widthLeft -= $orientatedItem->getWidth();
189
190
                $rowWidth += $orientatedItem->getWidth();
191
                $rowLength = max($rowLength, $orientatedItem->getLength());
192
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
193
194
                //allow items to be stacked in place within the same footprint up to current layer depth
195
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
196
                $this->tryAndStackItemsIntoSpace($layer, $prevItem, $this->items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth(), $rowLength);
197
                $x += $orientatedItem->getWidth();
198
199
                $prevItem = $packedItem;
200
                if ($this->items->count() === 0) {
201
                    $this->rebuildItemList();
202
                }
203
            } elseif (count($layer->getItems()) === 0) { // zero items on layer
204
                $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
205
                continue;
206
            } elseif ($widthLeft > 0 && $this->items->count() > 0) { // skip for now, move on to the next item
207
                $this->logger->debug("doesn't fit, skipping for now");
208
                $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
                while ($this->items->count() > 2 && $this->orientatedItemFactory->isSameDimensions($itemToPack, $this->items->top())) {
211
                    $this->skippedItems[] = $this->items->extract();
212
                }
213
            } elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength(), $itemToPack->getDepth())) {
214
                $this->logger->debug('No more fit in width wise, resetting for new row');
215
                $widthLeft += $rowWidth;
216
                $lengthLeft -= $rowLength;
217
                $y += $rowLength;
218
                $x = $rowWidth = $rowLength = 0;
219
                $this->skippedItems[] = $itemToPack;
220
                $this->rebuildItemList();
221
                $prevItem = null;
222
                continue;
223
            } else {
224
                $this->logger->debug('no items fit, so starting next vertical layer');
225
                $this->skippedItems[] = $itemToPack;
226
                $this->rebuildItemList();
227
228
                return;
229
            }
230
        }
231
    }
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
    public function stabiliseLayers()
240
    {
241
        $stabiliser = new LayerStabiliser();
242
        $this->layers = $stabiliser->stabilise($this->layers);
243
    }
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
    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
        $this->logger->debug(
274
            "evaluating item {$itemToPack->getDescription()} for fit",
275
            [
276
                'item' => $itemToPack,
277
                'space' => [
278
                    'maxWidth' => $maxWidth,
279
                    'maxLength' => $maxLength,
280
                    'maxDepth' => $maxDepth,
281
                ],
282
            ]
283
        );
284
285
        $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
286
        $prevPackedItemList = $itemToPack instanceof ConstrainedPlacementItem ? $this->getPackedItemList() : new PackedItemList(); // don't calculate it if not going to be used
287
288
        $orientatedItemDecision = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth, $rowLength, $x, $y, $z, $prevPackedItemList);
289
290
        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
    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
        while ($this->items->count() > 0 && $this->checkNonDimensionalConstraints($this->items->top())) {
320
            $stackedItem = $this->getOrientationForItem(
321
                $this->items->top(),
322
                $prevItem,
323
                $nextItems,
324
                $this->items->count() === 1,
325
                $maxWidth,
326
                $maxLength,
327
                $maxDepth,
328
                $rowLength,
329
                $x,
330
                $y,
331
                $z
332
            );
333
            if ($stackedItem) {
334
                $this->remainingWeight -= $this->items->top()->getWeight();
335
                $layer->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $z));
336
                $this->items->extract();
337
                $maxDepth -= $stackedItem->getDepth();
338
                $z += $stackedItem->getDepth();
339
            } else {
340
                break;
341
            }
342
        }
343
    }
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
    protected function checkNonDimensionalConstraints(Item $itemToPack)
354
    {
355
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
356
357
        if ($itemToPack instanceof ConstrainedItem) {
358
            return $weightOK && $itemToPack->canBePackedInBox($this->getPackedItemList()->asItemList(), $this->box);
359
        }
360
361
        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
    protected function rebuildItemList()
383
    {
384
        while(count($this->skippedItems)) {
385
            $this->items->insert(array_pop($this->skippedItems));
386
        }
387
    }
388
389
    /**
390
     * Swap back width/length of the packed items to match orientation of the box if needed.
391
     */
392
    protected function rotateLayersNinetyDegrees()
393
    {
394
        foreach ($this->layers as $i => $originalLayer) {
395
            $newLayer = new PackedLayer();
396
            foreach ($originalLayer->getItems() as $item) {
397
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
398
                $newLayer->insert($packedItem);
399
            }
400
            $this->layers[$i] = $newLayer;
401
        }
402
    }
403
404
    /**
405
     * Are there items left to pack?
406
     *
407
     * @return bool
408
     */
409
    protected function hasItemsLeftToPack()
410
    {
411
        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
    protected function getPackedItemList()
420
    {
421
        $packedItemList = new PackedItemList();
422
        foreach ($this->layers as $layer) {
423
            foreach ($layer->getItems() as $packedItem) {
424
                $packedItemList->insert($packedItem);
425
            }
426
        }
427
428
        return $packedItemList;
429
    }
430
431
    /**
432
     * Return the current packed depth.
433
     *
434
     * @return int
435
     */
436
    protected function getCurrentPackedDepth()
437
    {
438
        $depth = 0;
439
        foreach ($this->layers as $layer) {
440
            $depth += $layer->getDepth();
441
        }
442
443
        return $depth;
444
    }
445
}
446