Completed
Push — extract_logic ( 249126...a297ff )
by Doug
13:02
created

VolumePacker.php (4 issues)

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($itemToPack, $prevItem, $nextItem, $this->widthLeft, $this->lengthLeft, $this->depthLeft);
136
137 29
            if ($orientatedItem) {
138
139 29
                $packedItems->insert($orientatedItem->getItem());
140 29
                $this->remainingWeight -= $itemToPack->getWeight();
141
142 29
                $this->lengthLeft -= $orientatedItem->getLength();
143 29
                $layerLength += $orientatedItem->getLength();
144 29
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
145
146 29
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
147
148 29
                $this->usedLength = max($this->usedLength, $layerLength);
149 29
                $this->usedWidth = max($this->usedWidth, $layerWidth);
150
151
                //allow items to be stacked in place within the same footprint up to current layerdepth
152 29
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
153 29
                $this->tryAndStackItemsIntoSpace($packedItems, $prevItem, $nextItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth);
154
155 29
                $prevItem = $orientatedItem;
156
157 29
                if (!$nextItem) {
158 29
                    $this->usedDepth += $layerDepth;
159
                }
160
            } else {
161
162 24
                $prevItem = null;
163
164 24
                if ($this->widthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
165 23
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
166 23
                    $this->lengthLeft += $layerLength;
167 23
                    $this->widthLeft -= $layerWidth;
168 23
                    $layerWidth = $layerLength = 0;
169 23
                    $this->items->insert($itemToPack);
170 23
                    continue;
171 18
                } elseif ($this->lengthLeft < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
172 7
                    $this->logger->debug("doesn't fit on layer even when empty");
173 7
                    continue;
174
                }
175
176 17
                $this->widthLeft = $layerWidth ? min(floor($layerWidth * 1.1), $this->box->getInnerWidth()) : $this->box->getInnerWidth();
0 ignored issues
show
Documentation Bug introduced by
It seems like $layerWidth ? min(floor(...s->box->getInnerWidth() can also be of type double. However, the property $widthLeft is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
177 17
                $this->lengthLeft = $layerLength ? min(floor($layerLength * 1.1), $this->box->getInnerLength()) : $this->box->getInnerLength();
0 ignored issues
show
Documentation Bug introduced by
It seems like $layerLength ? min(floor...->box->getInnerLength() can also be of type double. However, the property $lengthLeft is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
178 17
                $this->depthLeft -= $layerDepth;
179 17
                $this->usedDepth += $layerDepth;
180
181 17
                $layerWidth = $layerLength = $layerDepth = 0;
182 17
                $this->logger->debug("doesn't fit, so starting next vertical layer");
183 17
                $this->items->insert($itemToPack);
184
            }
185
        }
186 29
        $this->logger->debug("done with this box");
187 29
        return new PackedBox(
188 29
            $this->box,
189
            $packedItems,
190 29
            $this->widthLeft,
191 29
            $this->lengthLeft,
192 29
            $this->depthLeft,
193 29
            $this->remainingWeight,
194 29
            $this->usedWidth,
195 29
            $this->usedLength,
196 29
            $this->usedDepth);
197
    }
198
199
    /**
200
     * Get the best orientation for an item
201
     * @param Item $item
202
     * @param OrientatedItem|null $prevItem
203
     * @param Item|null $nextItem
204
     * @param int $widthLeft
205
     * @param int $lengthLeft
206
     * @param int $depthLeft
207
     *
208
     * @return OrientatedItem|false
209
     */
210 29
    protected function findBestOrientation(
211
        Item $item,
212
        OrientatedItem $prevItem = null,
213
        Item $nextItem = null,
214
        $widthLeft,
215
        $lengthLeft,
216
        $depthLeft) {
217
218 29
        $possibleOrientations = $this->findAllPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
219 29
        $orientationsToUse = $this->filterStableOrientations($possibleOrientations, $item, $prevItem, $nextItem);
220
221 29
        $orientationFits = [];
222
        /** @var OrientatedItem $orientation */
223 29
        foreach ($orientationsToUse as $o => $orientation) {
224 29
            $orientationFit = min($widthLeft - $orientation->getWidth(), $lengthLeft - $orientation->getLength());
225 29
            $orientationFits[$o] = $orientationFit;
226
        }
227
228 29
        if (!empty($orientationFits)) {
229 29
            asort($orientationFits);
230 29
            reset($orientationFits);
231 29
            $bestFit = $orientationsToUse[key($orientationFits)];
232 29
            $this->logger->debug("Selected best fit orientation", ['orientation' => $bestFit]);
233 29
            return $bestFit;
234
        } else {
235 27
            return false;
236
        }
237
    }
238
239
    /**
240
     * Find all possible orientations for an item
241
     * @param Item $item
242
     * @param OrientatedItem|null $prevItem
243
     * @param int $widthLeft
244
     * @param int $lengthLeft
245
     * @param int $depthLeft
246
     *
247
     * @return OrientatedItem[]
248
     */
249 29
    protected function findAllPossibleOrientations(
250
        Item $item,
251
        OrientatedItem $prevItem = null,
252
        $widthLeft,
253
        $lengthLeft,
254
        $depthLeft) {
255
256 29
        $orientations = [];
257
258
        //Special case items that are the same as what we just packed - keep orientation
259 29
        if ($prevItem && $prevItem->getItem() == $item) {
260 12
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
261
        } else {
262
263
            //simple 2D rotation
264 29
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
265 29
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
266
267
            //add 3D rotation if we're allowed
268 29
            if (!$item->getKeepFlat()) {
269 10
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
270 10
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
271 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
272 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
273
            }
274
        }
275
276
        //remove any that simply don't fit
277
        return array_filter($orientations, function(OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
278 29
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
279 29
        });
280
281
    }
282
283
    /**
284
     * Filter a set of orientations allow only those that have a stable base OR
285
     * the item doesn't fit in the box any other way
286
     * the item is the last one left to pack
287
     *
288
     * @param OrientatedItem[] $orientations
289
     * @param Item $item
290
     * @param OrientatedItem|null $prevItem
291
     * @param Item|null $nextItem
292
     *
293
     * @return OrientatedItem[]
294
     */
295 29
    protected function filterStableOrientations(
296
        array $orientations,
297
        Item $item,
298
        OrientatedItem $prevItem = null,
299
        Item $nextItem = null
300
    ) {
301 29
        $stableOrientations = $unstableOrientations = $orientationsToUse = [];
302
303 29
        foreach ($orientations as $o => $orientation) {
304 29
            if ($orientation->isStable()) {
305 26
                $stableOrientations[] = $orientation;
306
            } else {
307 29
                $unstableOrientations[] = $orientation;
308
            }
309
        }
310
311 29
        if (count($stableOrientations) > 0) {
312 26
            $orientationsToUse = $stableOrientations;
313 28
        } else if (count($unstableOrientations) > 0) {
314 4
            $orientationsInEmptyBox = $this->findAllPossibleOrientations(
315
                $item,
316
                $prevItem,
317 4
                $this->box->getInnerWidth(),
318 4
                $this->box->getInnerLength(),
319 4
                $this->box->getInnerDepth()
320
            );
321
322 4
            $stableOrientationsInEmptyBox = array_filter(
323
                $orientationsInEmptyBox,
324 4
                function(OrientatedItem $orientation) {
325 4
                    return $orientation->isStable();
326 4
                }
327
            );
328
329 4
            if (is_null($nextItem) || count($stableOrientationsInEmptyBox) == 0) {
330 4
                $orientationsToUse = $unstableOrientations;
331
            }
332
        }
333
334 29
        return $orientationsToUse;
335
    }
336
337
    /**
338
     * Figure out if we can stack the next item vertically on top of this rather than side by side
339
     * Used when we've packed a tall item, and have just put a shorter one next to it
340
     *
341
     * @param ItemList       $packedItems
342
     * @param OrientatedItem $prevItem
0 ignored issues
show
Should the type for parameter $prevItem not be null|OrientatedItem?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
343
     * @param Item           $nextItem
0 ignored issues
show
Should the type for parameter $nextItem not be null|Item?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
344
     * @param int            $maxWidth
345
     * @param int            $maxLength
346
     * @param int            $maxDepth
347
     */
348 29
    protected function tryAndStackItemsIntoSpace(ItemList $packedItems, OrientatedItem $prevItem = null, Item $nextItem = null, $maxWidth, $maxLength, $maxDepth)
349
    {
350 29
        while (!$this->items->isEmpty() && $this->remainingWeight >= $this->items->top()->getWeight()) {
351 27
            $stackedItem = $this->findBestOrientation($this->items->top(), $prevItem, $nextItem, $maxWidth, $maxLength, $maxDepth);
352 27
            if ($stackedItem) {
353 1
                $this->remainingWeight -= $this->items->top()->getWeight();
354 1
                $maxDepth -= $stackedItem->getDepth();
355 1
                $packedItems->insert($this->items->extract());
356
            } else {
357 27
                break;
358
            }
359
        }
360
    }
361
362
    /**
363
     * @param int $layerWidth
364
     * @param int $layerLength
365
     * @param int $layerDepth
366
     *
367
     * @return bool
368
     */
369 24
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth) {
370 24
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
371
    }
372
}
373