Completed
Push — 2.x-dev ( 03f1cc...530003 )
by Doug
45:33 queued 44:04
created

VolumePacker::rebuildItemList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
crap 2
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