Completed
Push — 2.x-dev ( 03f1cc...530003 )
by Doug
45:33 queued 44:04
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
     * Constructor
59
     *
60
     * @param Box      $box
61
     * @param ItemList $items
62
     */
63 34
    public function __construct(Box $box, ItemList $items)
64
    {
65 34
        $this->box = $box;
66 34
        $this->items = $items;
67
68 34
        $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
69 34
        $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
70 34
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
71 34
        $this->skippedItems = new ItemList();
72 34
        $this->logger = new NullLogger();
73
    }
74
75
    /**
76
     * Pack as many items as possible into specific given box
77
     *
78
     * @return PackedBox packed box
79
     */
80 34
    public function pack()
81
    {
82 34
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
83
84 34
        $packedItems = new PackedItemList;
85 34
        $prevItem = null;
86
87 34
        $x = $y = $z = $rowWidth = $rowLength = $layerWidth = $layerLength = $layerDepth = 0;
88
89 34
        $packingWidthLeft = $this->boxWidth;
90 34
        $packingLengthLeft = $this->boxLength;
91 34
        $packingDepthLeft = $this->box->getInnerDepth();
92
93 34
        while (!$this->items->isEmpty()) {
94 34
            $itemToPack = $this->items->extract();
95 34
            $isLastItem = $this->skippedItems->isEmpty() && $this->items->isEmpty();
96
97
            //skip items that are simply too heavy or too large
98 34
            if (!$this->checkConstraints($itemToPack, $packedItems)) {
99 9
                $this->rebuildItemList();
100 9
                continue;
101
            }
102
103 34
            $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $isLastItem, $packingWidthLeft, $packingLengthLeft, $packingDepthLeft);
104
105 34
            if ($orientatedItem instanceof OrientatedItem) {
106 34
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
107 34
                $packedItems->insert($packedItem);
108 34
                $this->remainingWeight -= $orientatedItem->getItem()->getWeight();
109 34
                $packingWidthLeft -= $orientatedItem->getWidth();
110
111 34
                $rowWidth += $orientatedItem->getWidth();
112 34
                $rowLength = max($rowLength, $orientatedItem->getLength());
113 34
                $layerDepth = max($layerDepth, $orientatedItem->getDepth());
114
115
                //allow items to be stacked in place within the same footprint up to current layer depth
116 34
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
117 34
                $this->tryAndStackItemsIntoSpace($packedItems, $prevItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $z + $orientatedItem->getDepth());
118 34
                $x += $orientatedItem->getWidth();
119
120 34
                $prevItem = $packedItem;
121
122 34
                $this->rebuildItemList();
123
            } else {
124 24
                if ($layerWidth == 0 && $layerDepth == 0) { // zero items on layer
125 5
                    $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
126 5
                    $prevItem = null;
127 5
                    continue;
128 24
                } elseif (!$this->items->isEmpty()) { // skip for now, move on to the next item
129 20
                    $this->logger->debug("doesn't fit, skipping for now");
130 20
                    $this->skippedItems->insert($itemToPack);
131 24
                } elseif ($x > 0 && $packingLengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength())) {
132 24
                    $this->logger->debug("No more fit in width wise, resetting for new row");
133 24
                    $layerWidth = max($layerWidth, $rowWidth);
134 24
                    $layerLength += $rowLength;
135 24
                    $packingWidthLeft += $rowWidth;
136 24
                    $packingLengthLeft -= $rowLength;
137 24
                    $y += $rowLength;
138 24
                    $x = $rowWidth = $rowLength = 0;
139 24
                    $this->rebuildItemList();
140 24
                    $this->items->insert($itemToPack);
141 24
                    $prevItem = null;
142 24
                    continue;
143
                } else {
144 17
                    $this->logger->debug("no items fit, so starting next vertical layer");
145
146 17
                    $layerWidth = max($layerWidth, $rowWidth);
147 17
                    $layerLength += $rowLength;
148 17
                    $packingWidthLeft = $rowWidth ? min(floor($layerWidth * 1.1), $this->boxWidth) : $this->boxWidth;
149 17
                    $packingLengthLeft = $rowLength ? min(floor($layerLength * 1.1), $this->boxLength) : $this->boxLength;
150 17
                    $packingDepthLeft -= $layerDepth;
151
152 17
                    $z += $layerDepth;
153 17
                    $x = $y = $rowWidth = $rowLength = $layerWidth = $layerLength = $layerDepth = 0;
154
155 17
                    $this->rebuildItemList();
156 17
                    $this->items->insert($itemToPack);
157 17
                    $prevItem = null;
158
                }
159
            }
160
        }
161 34
        $this->logger->debug("done with this box");
162 34
        return PackedBox::fromPackedItemList($this->box, $packedItems);
163
    }
164
165
    /**
166
     * @param Item            $itemToPack
167
     * @param PackedItem|null $prevItem
168
     * @param bool            $isLastItem
169
     * @param int             $maxWidth
170
     * @param int             $maxLength
171
     * @param int             $maxDepth
172
     *
173
     * @return OrientatedItem|false
174
     */
175 34
    protected function getOrientationForItem(
176
        Item $itemToPack,
177
        PackedItem $prevItem = null,
178
        $isLastItem,
179
        $maxWidth,
180
        $maxLength,
181
        $maxDepth
182
    ) {
183 34
        $this->logger->debug(
184 34
            "evaluating item {$itemToPack->getDescription()} for fit",
185
            [
186 34
                'item' => $itemToPack,
187
                'space' => [
188 34
                    'maxWidth'    => $maxWidth,
189 34
                    'maxLength'   => $maxLength,
190 34
                    'maxDepth'    => $maxDepth,
191
                ]
192
            ]
193
        );
194
195 34
        $orientatedItemFactory = new OrientatedItemFactory();
196 34
        $orientatedItemFactory->setLogger($this->logger);
197 34
        $orientatedItem = $orientatedItemFactory->getBestOrientation($this->box, $itemToPack, $prevItem, $isLastItem, $maxWidth, $maxLength, $maxDepth);
198
199 34
        return $orientatedItem;
200
    }
201
202
    /**
203
     * Figure out if we can stack the next item vertically on top of this rather than side by side
204
     * Used when we've packed a tall item, and have just put a shorter one next to it
205
     *
206
     * @param PackedItemList $packedItems
207
     * @param PackedItem|null $prevItem
208
     * @param int $maxWidth
209
     * @param int $maxLength
210
     * @param int $maxDepth
211
     * @param int $x
212
     * @param int $y
213
     * @param int $z
214
     */
215 34
    protected function tryAndStackItemsIntoSpace(
216
        PackedItemList $packedItems,
217
        PackedItem $prevItem = null,
218
        $maxWidth,
219
        $maxLength,
220
        $maxDepth,
221
        $x,
222
        $y,
223
        $z
224
    ) {
225 34
        while (!$this->items->isEmpty() && $this->checkNonDimensionalConstraints($this->items->top(), $packedItems)) {
226 32
            $stackedItem = $this->getOrientationForItem(
227 32
                $this->items->top(),
228 32
                $prevItem,
229 32
                $this->items->count() === 1,
230 32
                $maxWidth,
231 32
                $maxLength,
232 32
                $maxDepth
233
            );
234 32
            if ($stackedItem) {
235 3
                $this->remainingWeight -= $this->items->top()->getWeight();
236 3
                $packedItems->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $z));
237 3
                $this->items->extract();
238 3
                $maxDepth -= $stackedItem->getDepth();
239 3
                $z += $stackedItem->getDepth();
240
            } else {
241 32
                break;
242
            }
243
        }
244
    }
245
246
    /**
247
     * Check item generally fits into box
248
     *
249
     * @param Item            $itemToPack
250
     * @param PackedItemList  $packedItems
251
     *
252
     * @return bool
253
     */
254 34
    protected function checkConstraints(
255
        Item $itemToPack,
256
        PackedItemList $packedItems
257
    ) {
258 34
        return $this->checkNonDimensionalConstraints($itemToPack, $packedItems) &&
259 34
               $this->checkDimensionalConstraints($itemToPack);
260
    }
261
262
    /**
263
     * As well as purely dimensional constraints, there are other constraints that need to be met
264
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box)
265
     *
266
     * @param Item     $itemToPack
267
     * @param PackedItemList $packedItems
268
     *
269
     * @return bool
270
     */
271 34
    protected function checkNonDimensionalConstraints(Item $itemToPack, PackedItemList $packedItems)
272
    {
273 34
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
274
275 34
        if ($itemToPack instanceof ConstrainedItem) {
276 1
            return $weightOK && $itemToPack->canBePackedInBox($packedItems->asItemList(), $this->box);
277
        }
278
279 33
        return $weightOK;
280
    }
281
282
    /**
283
     * Check the item physically fits in the box (at all)
284
     *
285
     * @param Item            $itemToPack
286
     *
287
     * @return bool
288
     */
289 34
    protected function checkDimensionalConstraints(Item $itemToPack)
290
    {
291 34
        $orientatedItemFactory = new OrientatedItemFactory();
292 34
        $orientatedItemFactory->setLogger($this->logger);
293 34
        return !!$orientatedItemFactory->getPossibleOrientationsInEmptyBox($itemToPack, $this->box);
294
    }
295
296
    /**
297
     * Reintegrate skipped items into main list when nothing left to process
298
     */
299 34
    protected function rebuildItemList() {
300 34
        if ($this->items->isEmpty()) {
301 34
            $this->items = $this->skippedItems;
302 34
            $this->skippedItems = new ItemList();
303
        }
304
    }
305
}
306