Completed
Push — master ( 349d65...0b9f3a )
by Doug
12:56
created

VolumePacker::findBestOrientation()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 17
cts 17
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 17
nc 5
nop 6
crap 6
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 27
    public function __construct(Box $box, ItemList $items)
80
    {
81 27
        $this->logger = new NullLogger();
82
83 27
        $this->box = $box;
84 27
        $this->items = $items;
85
86 27
        $this->depthLeft = $this->box->getInnerDepth();
87 27
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
88 27
        $this->widthLeft = $this->box->getInnerWidth();
89 27
        $this->lengthLeft = $this->box->getInnerLength();
90 27
    }
91
92
    /**
93
     * Pack as many items as possible into specific given box
94
     * @return PackedBox packed box
95
     */
96 27
    public function pack()
97
    {
98 27
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
99
100 27
        $packedItems = new ItemList;
101
102 27
        $layerWidth = $layerLength = $layerDepth = 0;
103
104 27
        $prevItem = null;
105
106 27
        while (!$this->items->isEmpty()) {
107
108 27
            $itemToPack = $this->items->extract();
109
110
            //skip items that are simply too heavy
111 27
            if ($itemToPack->getWeight() > $this->remainingWeight) {
112 4
                continue;
113
            }
114
115 27
            $this->logger->debug(
116 27
                "evaluating item {$itemToPack->getDescription()}",
117
                [
118 27
                    'item' => $itemToPack,
119
                    'space' => [
120 27
                        'widthLeft'   => $this->widthLeft,
121 27
                        'lengthLeft'  => $this->lengthLeft,
122 27
                        'depthLeft'   => $this->depthLeft,
123 27
                        'layerWidth'  => $layerWidth,
124 27
                        'layerLength' => $layerLength,
125
                        'layerDepth'  => $layerDepth
126 27
                    ]
127 27
                ]
128 27
            );
129
130 27
            $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
131 27
            $orientatedItem = $this->findBestOrientation($itemToPack, $prevItem, $nextItem, $this->widthLeft, $this->lengthLeft, $this->depthLeft);
132
133 27
            if ($orientatedItem) {
134
135 27
                $packedItems->insert($orientatedItem->getItem());
136 27
                $this->remainingWeight -= $itemToPack->getWeight();
137
138 27
                $this->lengthLeft -= $orientatedItem->getLength();
139 27
                $layerLength += $orientatedItem->getLength();
140 27
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
141
142 27
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
143
144 27
                $this->usedLength = max($this->usedLength, $layerLength);
145 27
                $this->usedWidth = max($this->usedWidth, $layerWidth);
146
147
                //allow items to be stacked in place within the same footprint up to current layerdepth
148 27
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
149 27
                $this->tryAndStackItemsIntoSpace($packedItems, $prevItem, $nextItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth);
150
151 27
                $prevItem = $orientatedItem;
152
153 27
                if (!$nextItem) {
154 24
                    $this->usedDepth += $layerDepth;
155 24
                }
156 27
            } else {
157
158 23
                $prevItem = null;
159
160 23
                if ($this->widthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
161 22
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
162 22
                    $this->lengthLeft += $layerLength;
163 22
                    $this->widthLeft -= $layerWidth;
164 22
                    $layerWidth = $layerLength = 0;
165 22
                    $this->items->insert($itemToPack);
166 22
                    continue;
167 18
                } elseif ($this->lengthLeft < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
168 7
                    $this->logger->debug("doesn't fit on layer even when empty");
169 7
                    continue;
170
                }
171
172 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...
173 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...
174 17
                $this->depthLeft -= $layerDepth;
175 17
                $this->usedDepth += $layerDepth;
176
177 17
                $layerWidth = $layerLength = $layerDepth = 0;
178 17
                $this->logger->debug("doesn't fit, so starting next vertical layer");
179 17
                $this->items->insert($itemToPack);
180
            }
181 27
        }
182 27
        $this->logger->debug("done with this box");
183 27
        return new PackedBox(
184 27
            $this->box,
185 27
            $packedItems,
186 27
            $this->widthLeft,
187 27
            $this->lengthLeft,
188 27
            $this->depthLeft,
189 27
            $this->remainingWeight,
190 27
            $this->usedWidth,
191 27
            $this->usedLength,
192 27
            $this->usedDepth);
193
    }
194
195
    /**
196
     * Get the best orientation for an item
197
     * @param Item $item
198
     * @param OrientatedItem|null $prevItem
199
     * @param Item|null $nextItem
200
     * @param int $widthLeft
201
     * @param int $lengthLeft
202
     * @param int $depthLeft
203
     * @return OrientatedItem|false
204
     */
205 27
    protected function findBestOrientation(Item $item, OrientatedItem $prevItem = null, Item $nextItem = null, $widthLeft, $lengthLeft, $depthLeft) {
206
207 27
        $orientations = $this->findPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
208
209
        // special casing based on next item
210 27
        if (isset($orientations[0]) && $nextItem == $item && $lengthLeft >= 2 * $item->getLength()) {
211 6
            $this->logger->debug("not rotating based on next item");
212 6
            return $orientations[0]; // XXX this is tied to the ordering from ->findPossibleOrientations()
213
        }
214
215 27
        $orientationFits = [];
216
217
        /** @var OrientatedItem $orientation */
218 27
        foreach ($orientations as $o => $orientation) {
219 27
            $orientationFit = min($widthLeft   - $orientation->getWidth(), $lengthLeft  - $orientation->getLength());
220 27
            $orientationFits[$o] = $orientationFit;
221 27
        }
222
223 27
        if (!empty($orientationFits)) {
224 27
            asort($orientationFits);
225 27
            reset($orientationFits);
226 27
            $bestFit = key($orientationFits);
227 27
            $this->logger->debug("Using orientation #{$bestFit}");
228 27
            return $orientations[$bestFit];
229
        } else {
230 25
            return false;
231
        }
232
    }
233
234
    /**
235
     * Find all possible orientations for an item
236
     * @param Item $item
237
     * @param OrientatedItem|null $prevItem
238
     * @param int $widthLeft
239
     * @param int $lengthLeft
240
     * @param int $depthLeft
241
     * @return OrientatedItem[]
242
     */
243 27
    protected function findPossibleOrientations(Item $item, OrientatedItem $prevItem = null, $widthLeft, $lengthLeft, $depthLeft) {
244
245 27
        $orientations = [];
246
247
        //Special case items that are the same as what we just packed - keep orientation
248 27
        if ($prevItem && $prevItem->getItem() == $item) {
249 10
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
250 10
        } else {
251
252
            //simple 2D rotation
253 27
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
254 27
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
255
256
            //add 3D rotation if we're allowed
257 27
            if (!$item->getKeepFlat()) {
258 10
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
259 10
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
260 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
261 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
262 10
            }
263
        }
264
265
        //remove any that simply don't fit
266 27
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
267 27
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
268 27
        });
269
270
    }
271
272
    /**
273
     * Figure out if we can stack the next item vertically on top of this rather than side by side
274
     * Used when we've packed a tall item, and have just put a shorter one next to it
275
     *
276
     * @param ItemList       $packedItems
277
     * @param OrientatedItem $prevItem
0 ignored issues
show
Documentation introduced by
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...
278
     * @param Item           $nextItem
0 ignored issues
show
Documentation introduced by
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...
279
     * @param int            $maxWidth
280
     * @param int            $maxLength
281
     * @param int            $maxDepth
282
     */
283 27
    protected function tryAndStackItemsIntoSpace(ItemList $packedItems, OrientatedItem $prevItem = null, Item $nextItem = null, $maxWidth, $maxLength, $maxDepth)
284
    {
285 27
        while (!$this->items->isEmpty() && $this->remainingWeight >= $this->items->top()->getWeight()) {
286 25
            $stackedItem = $this->findBestOrientation($this->items->top(), $prevItem, $nextItem, $maxWidth, $maxLength, $maxDepth);
287 25
            if ($stackedItem) {
288 1
                $this->remainingWeight -= $this->items->top()->getWeight();
289 1
                $maxDepth -= $stackedItem->getDepth();
290 1
                $packedItems->insert($this->items->extract());
291 1
            } else {
292 25
                break;
293
            }
294 1
        }
295 27
    }
296
297
    /**
298
     * @param int $layerWidth
299
     * @param int $layerLength
300
     * @param int $layerDepth
301
     * @return bool
302
     */
303 23
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth) {
304 23
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
305
    }
306
}
307