Completed
Push — 1.x-dev ( e3d217...fa93b4 )
by Doug
16:45 queued 11:21
created

VolumePacker::tryAndStackItemsIntoSpace()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 30
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 16
cts 16
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 25
nc 3
nop 8
crap 4

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
 * @package BoxPacker
5
 * @author Doug Wright
6
 */
7
namespace DVDoug\BoxPacker;
8
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerAwareTrait;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\NullLogger;
13
14
/**
15
 * Actual packer
16
 * @author Doug Wright
17
 * @package BoxPacker
18
 */
19
class VolumePacker implements LoggerAwareInterface
20
{
21
    use LoggerAwareTrait;
22
23
    /**
24
     * Box to pack items into
25
     * @var Box
26
     */
27
    protected $box;
28
29
    /**
30
     * @var int
31
     */
32
    protected $boxWidth;
33
34
    /**
35
     * @var int
36
     */
37
    protected $boxLength;
38
39
    /**
40
     * List of items to be packed
41
     * @var ItemList
42
     */
43
    protected $items;
44
45
    /**
46
     * List of items to be packed
47
     * @var ItemList
48
     */
49
    protected $skippedItems;
50
51
    /**
52
     * Remaining weight capacity of the box
53
     * @var int
54
     */
55
    protected $remainingWeight;
56
57
    /**
58
     * Whether the box was rotated for packing
59
     * @var bool
60
     */
61
    protected $boxRotated = false;
62
63
    /**
64
     * Constructor
65
     *
66
     * @param Box      $box
67
     * @param ItemList $items
68
     */
69 34
    public function __construct(Box $box, ItemList $items)
70
    {
71 34
        $this->box = $box;
72 34
        $this->items = $items;
73
74 34
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
75 34
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
76 34
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
77 34
        $this->skippedItems = new ItemList();
78 34
        $this->logger = new NullLogger();
79
80
        // we may have just rotated the box for packing purposes, record if we did
81 34
        if ($this->box->getInnerWidth() != $this->boxWidth || $this->box->getInnerLength() != $this->boxLength) {
82 11
            $this->boxRotated = true;
83
        }
84
85
    }
86
87
    /**
88
     * Pack as many items as possible into specific given box
89
     *
90
     * @return PackedBox packed box
91
     */
92 34
    public function pack()
93
    {
94 34
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
95
96 34
        $packedItems = new PackedItemList;
97 34
        $prevItem = null;
98
99 34
        $x = $y = $z = $rowWidth = $rowLength = $layerWidth = $layerLength = $layerDepth = 0;
100
101 34
        $packingWidthLeft = $this->boxWidth;
102 34
        $packingLengthLeft = $this->boxLength;
103 34
        $packingDepthLeft = $this->box->getInnerDepth();
104
105 34
        while (!$this->items->isEmpty()) {
106 34
            $itemToPack = $this->items->extract();
107 34
            $isLastItem = $this->skippedItems->isEmpty() && $this->items->isEmpty();
108
109
            //skip items that are simply too heavy or too large
110 34
            if (!$this->checkConstraints($itemToPack, $packedItems)) {
111 9
                $this->rebuildItemList();
112 9
                continue;
113
            }
114
115 34
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $isLastItem, $packingWidthLeft, $packingLengthLeft, $packingDepthLeft);
116
117 34
            if ($orientatedItem instanceof OrientatedItem) {
118 34
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
119 34
                $packedItems->insert($packedItem);
120 34
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
121 34
                $packingWidthLeft -= $orientatedItem->getWidth();
122
123 34
                $rowWidth += $orientatedItem->getWidth();
124 34
                $rowLength = max($rowLength, $orientatedItem->getLength());
125 34
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
126
127
                //allow items to be stacked in place within the same footprint up to current layer depth
128 34
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
129 34
                $this->tryAndStackItemsIntoSpace($packedItems, $prevItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $z + $orientatedItem->getDepth());
130 34
                $x += $orientatedItem->getWidth();
131
132 34
                $prevItem = $packedItem;
133
134 34
                $this->rebuildItemList();
135
            } else {
136 25
                if ($layerWidth == 0 && $layerDepth == 0) { // zero items on layer
137 5
                    $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
138 5
                    $prevItem = null;
139 5
                    continue;
140 25
                } elseif (!$this->items->isEmpty()) { // skip for now, move on to the next item
141 21
                    $this->logger->debug("doesn't fit, skipping for now");
142 21
                    $this->skippedItems->insert($itemToPack);
143 25
                } elseif ($x > 0 && $packingLengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength())) {
144 25
                    $this->logger->debug("No more fit in width wise, resetting for new row");
145 25
                    $layerWidth = max($layerWidth, $rowWidth);
146 25
                    $layerLength += $rowLength;
147 25
                    $packingWidthLeft += $rowWidth;
148 25
                    $packingLengthLeft -= $rowLength;
149 25
                    $y += $rowLength;
150 25
                    $x = $rowWidth = $rowLength = 0;
151 25
                    $this->rebuildItemList();
152 25
                    $this->items->insert($itemToPack);
153 25
                    $prevItem = null;
154 25
                    continue;
155
                } else {
156 18
                    $this->logger->debug("no items fit, so starting next vertical layer");
157
158 18
                    $layerWidth = max($layerWidth, $rowWidth);
159 18
                    $layerLength += $rowLength;
160 18
                    $packingWidthLeft = $rowWidth ? min(floor($layerWidth * 1.1), $this->boxWidth) : $this->boxWidth;
161 18
                    $packingLengthLeft = $rowLength ? min(floor($layerLength * 1.1), $this->boxLength) : $this->boxLength;
162 18
                    $packingDepthLeft -= $layerDepth;
163
164 18
                    $z += $layerDepth;
165 18
                    $x = $y = $rowWidth = $rowLength = $layerWidth = $layerLength = $layerDepth = 0;
166
167 18
                    $this->rebuildItemList();
168 18
                    $this->items->insert($itemToPack);
169 18
                    $prevItem = null;
170
                }
171
            }
172
        }
173 34
        $this->logger->debug("done with this box");
174 34
        return $this->createPackedBox($packedItems);
175
    }
176
177
    /**
178
     * @param Item            $itemToPack
179
     * @param PackedItem|null $prevItem
180
     * @param bool            $isLastItem
181
     * @param int             $maxWidth
182
     * @param int             $maxLength
183
     * @param int             $maxDepth
184
     *
185
     * @return OrientatedItem|false
186
     */
187 34
    protected function getOrientationForItem(
188
        Item $itemToPack,
189
        PackedItem $prevItem = null,
190
        $isLastItem,
191
        $maxWidth,
192
        $maxLength,
193
        $maxDepth
194
    ) {
195 34
        $this->logger->debug(
196 34
            "evaluating item {$itemToPack->getDescription()} for fit",
197
            [
198 34
                'item' => $itemToPack,
199
                'space' => [
200 34
                    'maxWidth'    => $maxWidth,
201 34
                    'maxLength'   => $maxLength,
202 34
                    'maxDepth'    => $maxDepth,
203
                ],
204
            ]
205
        );
206
207 34
        $orientatedItemFactory = new OrientatedItemFactory();
208 34
        $orientatedItemFactory->setLogger($this->logger);
209 34
        $orientatedItem = $orientatedItemFactory->getBestOrientation($this->box, $itemToPack, $prevItem, $isLastItem, $maxWidth, $maxLength, $maxDepth);
210
211 34
        return $orientatedItem;
212
    }
213
214
    /**
215
     * Figure out if we can stack the next item vertically on top of this rather than side by side
216
     * Used when we've packed a tall item, and have just put a shorter one next to it
217
     *
218
     * @param PackedItemList $packedItems
219
     * @param PackedItem|null $prevItem
220
     * @param int $maxWidth
221
     * @param int $maxLength
222
     * @param int $maxDepth
223
     * @param int $x
224
     * @param int $y
225
     * @param int $z
226
     */
227 34
    protected function tryAndStackItemsIntoSpace(
228
        PackedItemList $packedItems,
229
        PackedItem $prevItem = null,
230
        $maxWidth,
231
        $maxLength,
232
        $maxDepth,
233
        $x,
234
        $y,
235
        $z
236
    ) {
237 34
        while (!$this->items->isEmpty() && $this->checkNonDimensionalConstraints($this->items->top(), $packedItems)) {
238 33
            $stackedItem = $this->getOrientationForItem(
239 33
                $this->items->top(),
240 33
                $prevItem,
241 33
                $this->items->count() === 1,
242 33
                $maxWidth,
243 33
                $maxLength,
244 33
                $maxDepth
245
            );
246 33
            if ($stackedItem) {
247 3
                $this->remainingWeight -= $this->items->top()->getWeight();
248 3
                $packedItems->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $z));
249 3
                $this->items->extract();
250 3
                $maxDepth -= $stackedItem->getDepth();
251 3
                $z += $stackedItem->getDepth();
252
            } else {
253 33
                break;
254
            }
255
        }
256
    }
257
258
    /**
259
     * Check item generally fits into box
260
     *
261
     * @param Item            $itemToPack
262
     * @param PackedItemList  $packedItems
263
     *
264
     * @return bool
265
     */
266 34
    protected function checkConstraints(
267
        Item $itemToPack,
268
        PackedItemList $packedItems
269
    ) {
270 34
        return $this->checkNonDimensionalConstraints($itemToPack, $packedItems) &&
271 34
               $this->checkDimensionalConstraints($itemToPack);
272
    }
273
274
    /**
275
     * As well as purely dimensional constraints, there are other constraints that need to be met
276
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box)
277
     *
278
     * @param Item     $itemToPack
279
     * @param PackedItemList $packedItems
280
     *
281
     * @return bool
282
     */
283 34
    protected function checkNonDimensionalConstraints(Item $itemToPack, PackedItemList $packedItems)
284
    {
285 34
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
286
287 34
        if ($itemToPack instanceof ConstrainedItem) {
288 1
            return $weightOK && $itemToPack->canBePackedInBox($packedItems->asItemList(), $this->box);
289
        }
290
291 33
        return $weightOK;
292
    }
293
294
    /**
295
     * Check the item physically fits in the box (at all)
296
     *
297
     * @param Item            $itemToPack
298
     *
299
     * @return bool
300
     */
301 34
    protected function checkDimensionalConstraints(Item $itemToPack)
302
    {
303 34
        $orientatedItemFactory = new OrientatedItemFactory();
304 34
        $orientatedItemFactory->setLogger($this->logger);
305 34
        return !!$orientatedItemFactory->getPossibleOrientationsInEmptyBox($itemToPack, $this->box);
306
    }
307
308
    /**
309
     * Reintegrate skipped items into main list when nothing left to process
310
     */
311 34
    protected function rebuildItemList() {
312 34
        if ($this->items->isEmpty()) {
313 34
            $this->items = $this->skippedItems;
314 34
            $this->skippedItems = new ItemList();
315
        }
316
    }
317
318
    /**
319
     * @param PackedItemList $packedItems
320
     *
321
     * @return PackedBox
322
     */
323 34
    protected function createPackedBox(PackedItemList $packedItems)
324
    {
325
        //if we rotated the box for packing, need to swap back width/length of the packed items
326 34
        if ($this->boxRotated) {
327 11
            $items = iterator_to_array($packedItems);
328 11
            $packedItems = new PackedItemList();
329
            /** @var PackedItem $item */
330 11
            foreach($items as $item) {
331 11
                $packedItems->insert(
332 11
                    new PackedItem(
333 11
                        $item->getItem(),
334 11
                        $item->getY(),
335 11
                        $item->getX(),
336 11
                        $item->getZ(),
337 11
                        $item->getLength(),
338 11
                        $item->getWidth(),
339 11
                        $item->getDepth()
340
                    )
341
                );
342
            }
343
        }
344
345 34
        return PackedBox::fromPackedItemList($this->box, $packedItems);
346
    }
347
}
348