Completed
Push — extract_logic ( 7c18c8...476e4a )
by Doug
11:12
created

VolumePacker.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\NullLogger;
12
13
/**
14
 * Actual packer
15
 * @author Doug Wright
16
 * @package BoxPacker
17
 */
18
class VolumePacker implements LoggerAwareInterface
19
{
20
    use LoggerAwareTrait;
21
22
    /**
23
     * Box to pack items into
24
     * @var Box
25
     */
26
    protected $box;
27
28
    /**
29
     * List of items to be packed
30
     * @var ItemList
31
     */
32
    protected $items;
33
34
    /**
35
     * Remaining width of the box to pack items into
36
     * @var int
37
     */
38
    protected $widthLeft;
39
40
    /**
41
     * Remaining length of the box to pack items into
42
     * @var int
43
     */
44
    protected $lengthLeft;
45
46
    /**
47
     * Remaining depth of the box to pack items into
48
     * @var int
49
     */
50
    protected $depthLeft;
51
52
    /**
53
     * Remaining weight capacity of the box
54
     * @var int
55
     */
56
    protected $remainingWeight;
57
58
    /**
59
     * Used width inside box for packing items
60
     * @var int
61
     */
62
    protected $usedWidth = 0;
63
64
    /**
65
     * Used length inside box for packing items
66
     * @var int
67
     */
68
    protected $usedLength = 0;
69
70
    /**
71
     * Used depth inside box for packing items
72
     * @var int
73
     */
74
    protected $usedDepth = 0;
75
76
    /**
77
     * Constructor
78
     *
79
     * @param Box      $box
80
     * @param ItemList $items
81
     */
82 29
    public function __construct(Box $box, ItemList $items)
83
    {
84 29
        $this->logger = new NullLogger();
85
86 29
        $this->box = $box;
87 29
        $this->items = $items;
88
89 29
        $this->depthLeft = $this->box->getInnerDepth();
90 29
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
91 29
        $this->widthLeft = $this->box->getInnerWidth();
92 29
        $this->lengthLeft = $this->box->getInnerLength();
93
    }
94
95
    /**
96
     * Pack as many items as possible into specific given box
97
     *
98
     * @return PackedBox packed box
99
     */
100 29
    public function pack()
101
    {
102 29
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
103
104 29
        $packedItems = new ItemList;
105
106 29
        $layerWidth = $layerLength = $layerDepth = 0;
107
108 29
        $prevItem = null;
109
110 29
        while (!$this->items->isEmpty()) {
111
112 29
            $itemToPack = $this->items->extract();
113
114
            //skip items that are simply too heavy
115 29
            if ($itemToPack->getWeight() > $this->remainingWeight) {
116 4
                continue;
117
            }
118
119 29
            $this->logger->debug(
120 29
                "evaluating item {$itemToPack->getDescription()}",
121
                [
122 29
                    'item' => $itemToPack,
123
                    'space' => [
124 29
                        'widthLeft'   => $this->widthLeft,
125 29
                        'lengthLeft'  => $this->lengthLeft,
126 29
                        'depthLeft'   => $this->depthLeft,
127 29
                        'layerWidth'  => $layerWidth,
128 29
                        'layerLength' => $layerLength,
129 29
                        'layerDepth'  => $layerDepth
130
                    ]
131
                ]
132
            );
133
134 29
            $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
135 29
            $orientatedItem = $this->findBestOrientation(
136
                $itemToPack,
137
                $prevItem,
138
                $nextItem,
139 29
                $this->widthLeft,
140 29
                $this->lengthLeft,
141 29
                $this->depthLeft
142
            );
143
144 29
            if ($orientatedItem) {
145
146 29
                $packedItems->insert($orientatedItem->getItem());
147 29
                $this->remainingWeight -= $itemToPack->getWeight();
148
149 29
                $this->lengthLeft -= $orientatedItem->getLength();
150 29
                $layerLength += $orientatedItem->getLength();
151 29
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
152
153 29
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
154
155 29
                $this->usedLength = max($this->usedLength, $layerLength);
156 29
                $this->usedWidth = max($this->usedWidth, $layerWidth);
157
158
                //allow items to be stacked in place within the same footprint up to current layerdepth
159 29
                $this->tryAndStackItemsIntoSpace(
160
                    $packedItems,
161
                    $prevItem,
162
                    $nextItem,
163
                    $orientatedItem,
164
                    $layerDepth);
165
166 29
                $prevItem = $orientatedItem;
167
168 29
                if (!$nextItem) {
169 29
                    $this->usedDepth += $layerDepth;
170
                }
171
            } else {
172
173 24
                $prevItem = null;
174
175 24
                if ($this->widthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
176 23
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
177 23
                    $this->lengthLeft += $layerLength;
178 23
                    $this->widthLeft -= $layerWidth;
179 23
                    $layerWidth = $layerLength = 0;
180 23
                    $this->items->insert($itemToPack);
181 23
                    continue;
182 18
                } elseif ($this->lengthLeft < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
183 7
                    $this->logger->debug("doesn't fit on layer even when empty");
184 7
                    continue;
185
                }
186
187 17
                $this->widthLeft = $layerWidth ? min(floor($layerWidth * 1.1), $this->box->getInnerWidth()) : $this->box->getInnerWidth();
188 17
                $this->lengthLeft = $layerLength ? min(floor($layerLength * 1.1), $this->box->getInnerLength()) : $this->box->getInnerLength();
189 17
                $this->depthLeft -= $layerDepth;
190 17
                $this->usedDepth += $layerDepth;
191
192 17
                $layerWidth = $layerLength = $layerDepth = 0;
193 17
                $this->logger->debug("doesn't fit, so starting next vertical layer");
194 17
                $this->items->insert($itemToPack);
195
            }
196
        }
197 29
        $this->logger->debug("done with this box");
198 29
        return new PackedBox(
199 29
            $this->box,
200
            $packedItems,
201 29
            $this->widthLeft,
202 29
            $this->lengthLeft,
203 29
            $this->depthLeft,
204 29
            $this->remainingWeight,
205 29
            $this->usedWidth,
206 29
            $this->usedLength,
207 29
            $this->usedDepth);
208
    }
209
210
    /**
211
     * Get the best orientation for an item
212
     * @param Item $item
213
     * @param OrientatedItem|null $prevItem
214
     * @param Item|null $nextItem
215
     * @param int $widthLeft
216
     * @param int $lengthLeft
217
     * @param int $depthLeft
218
     *
219
     * @return OrientatedItem|false
220
     */
221 29
    protected function findBestOrientation(
222
        Item $item,
223
        OrientatedItem $prevItem = null,
224
        Item $nextItem = null,
225
        $widthLeft,
226
        $lengthLeft,
227
        $depthLeft) {
228
229 29
        $possibleOrientations = $this->findAllPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
230 29
        $orientationsToUse = $this->filterStableOrientations($possibleOrientations, $item, $prevItem, $nextItem);
231
232 29
        $orientationFits = [];
233
        /** @var OrientatedItem $orientation */
234 29
        foreach ($orientationsToUse as $o => $orientation) {
235 29
            $orientationFit = min($widthLeft - $orientation->getWidth(), $lengthLeft - $orientation->getLength());
236 29
            $orientationFits[$o] = $orientationFit;
237
        }
238
239 29
        if (!empty($orientationFits)) {
240 29
            asort($orientationFits);
241 29
            reset($orientationFits);
242 29
            $bestFit = $orientationsToUse[key($orientationFits)];
243 29
            $this->logger->debug("Selected best fit orientation", ['orientation' => $bestFit]);
244 29
            return $bestFit;
245
        } else {
246 27
            return false;
247
        }
248
    }
249
250
    /**
251
     * Find all possible orientations for an item
252
     * @param Item $item
253
     * @param OrientatedItem|null $prevItem
254
     * @param int $widthLeft
255
     * @param int $lengthLeft
256
     * @param int $depthLeft
257
     *
258
     * @return OrientatedItem[]
259
     */
260 29
    protected function findAllPossibleOrientations(
261
        Item $item,
262
        OrientatedItem $prevItem = null,
263
        $widthLeft,
264
        $lengthLeft,
265
        $depthLeft) {
266
267 29
        $orientations = [];
268
269
        //Special case items that are the same as what we just packed - keep orientation
270 29
        if ($prevItem && $prevItem->getItem() == $item) {
271 12
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
272
        } else {
273
274
            //simple 2D rotation
275 29
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
276 29
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
277
278
            //add 3D rotation if we're allowed
279 29
            if (!$item->getKeepFlat()) {
280 10
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
281 10
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
282 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
283 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
284
            }
285
        }
286
287
        //remove any that simply don't fit
288
        return array_filter($orientations, function(OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
289 29
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
290 29
        });
291
292
    }
293
294
    /**
295
     * Filter a set of orientations allow only those that have a stable base OR
296
     * the item doesn't fit in the box any other way
297
     * the item is the last one left to pack
298
     *
299
     * @param OrientatedItem[] $orientations
300
     * @param Item $item
301
     * @param OrientatedItem|null $prevItem
302
     * @param Item|null $nextItem
303
     *
304
     * @return OrientatedItem[]
305
     */
306 29
    protected function filterStableOrientations(
307
        array $orientations,
308
        Item $item,
309
        OrientatedItem $prevItem = null,
310
        Item $nextItem = null
311
    ) {
312 29
        $stableOrientations = $unstableOrientations = $orientationsToUse = [];
313
314 29
        foreach ($orientations as $o => $orientation) {
315 29
            if ($orientation->isStable()) {
316 26
                $stableOrientations[] = $orientation;
317
            } else {
318 29
                $unstableOrientations[] = $orientation;
319
            }
320
        }
321
322 29
        if (count($stableOrientations) > 0) {
323 26
            $orientationsToUse = $stableOrientations;
324 28
        } else if (count($unstableOrientations) > 0) {
325 4
            $orientationsInEmptyBox = $this->findAllPossibleOrientations(
326
                $item,
327 4
                null,
328 4
                $this->box->getInnerWidth(),
329 4
                $this->box->getInnerLength(),
330 4
                $this->box->getInnerDepth()
331
            );
332
333 4
            $stableOrientationsInEmptyBox = array_filter(
334
                $orientationsInEmptyBox,
335 4
                function(OrientatedItem $orientation) {
336 4
                    return $orientation->isStable();
337 4
                }
338
            );
339
340 4
            if (is_null($nextItem) || count($stableOrientationsInEmptyBox) == 0) {
341 4
                $orientationsToUse = $unstableOrientations;
342
            }
343
        }
344
345 29
        return $orientationsToUse;
346
    }
347
348
    /**
349
     * Figure out if we can stack the next item vertically on top of this rather than side by side
350
     * Used when we've packed a tall item, and have just put a shorter one next to it
351
     *
352
     * @param ItemList       $packedItems
353
     * @param OrientatedItem $prevItem
354
     * @param Item           $nextItem
355
     * @param OrientatedItem $item
0 ignored issues
show
There is no parameter named $item. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
356
     */
357 29
    protected function tryAndStackItemsIntoSpace(
358
        ItemList $packedItems,
359
        OrientatedItem $prevItem = null,
360
        Item $nextItem = null,
361
        OrientatedItem $orientatedItem,
362
        $layerDepth)
363
    {
364 29
        $stackableDepth = $layerDepth - $orientatedItem->getDepth(); //we already packed this
365
366 29
        while (!$this->items->isEmpty() && $this->remainingWeight >= $this->items->top()->getWeight()) {
367 27
            $stackedItem = $this->findBestOrientation(
368 27
                $this->items->top(),
369
                $prevItem,
370
                $nextItem,
371 27
                $orientatedItem->getWidth(),
372 27
                $orientatedItem->getLength(),
373
                $stackableDepth);
374
375 27
            if ($stackedItem) {
376 1
                $this->remainingWeight -= $this->items->top()->getWeight();
377 1
                $stackableDepth -= $stackedItem->getDepth();
378 1
                $packedItems->insert($this->items->extract());
379
            } else {
380 27
                break;
381
            }
382
        }
383
    }
384
385
    /**
386
     * @param int $layerWidth
387
     * @param int $layerLength
388
     * @param int $layerDepth
389
     *
390
     * @return bool
391
     */
392 24
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth) {
393 24
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
394
    }
395
}
396