Completed
Push — master ( fe1eed...b375e4 )
by Doug
05:47
created

VolumePacker.php (2 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
     * Constructor
60
     */
61 26
    public function __construct(Box $box, ItemList $items)
62
    {
63 26
        $this->logger = new NullLogger();
64
65 26
        $this->box = $box;
66 26
        $this->items = $items;
67
68 26
        $this->depthLeft = $this->box->getInnerDepth();
69 26
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
70 26
        $this->widthLeft = $this->box->getInnerWidth();
71 26
        $this->lengthLeft = $this->box->getInnerLength();
72 26
    }
73
74
    /**
75
     * Pack as many items as possible into specific given box
76
     * @return PackedBox packed box
77
     */
78 26
    public function pack()
79
    {
80 26
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
81
82 26
        $packedItems = new ItemList;
83
84 26
        $layerWidth = $layerLength = $layerDepth = 0;
85
86 26
        $prevItem = null;
87
88 26
        while (!$this->items->isEmpty()) {
89
90 26
            $itemToPack = $this->items->extract();
91
92
            //skip items that are simply too heavy
93 26
            if ($itemToPack->getWeight() > $this->remainingWeight) {
94 4
                continue;
95
            }
96
97 26
            $this->logger->debug("evaluating item {$itemToPack->getDescription()}");
98 26
            $this->logger->debug("remaining width: {$this->widthLeft}, length: {$this->lengthLeft}, depth: {$this->depthLeft}");
99 26
            $this->logger->debug("layerWidth: {$layerWidth}, layerLength: {$layerLength}, layerDepth: {$layerDepth}");
100
101 26
            $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
102 26
            $orientatedItem = $this->findBestOrientation($itemToPack, $prevItem, $nextItem, $this->widthLeft, $this->lengthLeft, $this->depthLeft);
103
104 26
            if ($orientatedItem) {
105
106 26
                $packedItems->insert($orientatedItem->getItem());
107 26
                $this->remainingWeight -= $itemToPack->getWeight();
108
109 26
                $this->lengthLeft -= $orientatedItem->getLength();
110 26
                $layerLength += $orientatedItem->getLength();
111 26
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
112
113 26
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
114
115
                //allow items to be stacked in place within the same footprint up to current layerdepth
116 26
                $maxStackDepth = $layerDepth - $orientatedItem->getDepth();
117 26
                while (!$this->items->isEmpty() && $this->canStackItemInLayer($itemToPack, $this->items->top(), $maxStackDepth)) {
118 1
                    $this->remainingWeight -= $this->items->top()->getWeight();
119 1
                    $maxStackDepth -= $this->items->top()->getDepth(); // XXX no attempt at best fit
120 1
                    $packedItems->insert($this->items->extract());
121 1
                }
122
123 26
                $prevItem = $orientatedItem;
124 26
            } else {
125
126 23
                $prevItem = null;
127
128 23
                if ($this->widthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
129 22
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
130 22
                    $this->lengthLeft += $layerLength;
131 22
                    $this->widthLeft -= $layerWidth;
132 22
                    $layerWidth = $layerLength = 0;
133 22
                    $this->items->insert($itemToPack);
134 22
                    continue;
135 18
                } elseif ($this->lengthLeft < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
136 7
                    $this->logger->debug("doesn't fit on layer even when empty");
137 7
                    continue;
138
                }
139
140 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...
141 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...
142 17
                $this->depthLeft -= $layerDepth;
143
144 17
                $layerWidth = $layerLength = $layerDepth = 0;
145 17
                $this->logger->debug("doesn't fit, so starting next vertical layer");
146 17
                $this->items->insert($itemToPack);
147
            }
148 26
        }
149 26
        $this->logger->debug("done with this box");
150 26
        return new PackedBox($this->box, $packedItems, $this->widthLeft, $this->lengthLeft, $this->depthLeft, $this->remainingWeight);
151
    }
152
153
    /**
154
     * Get the best orientation for an item
155
     * @param Item $item
156
     * @param OrientatedItem|null $prevItem
157
     * @param Item|null $nextItem
158
     * @param int $widthLeft
159
     * @param int $lengthLeft
160
     * @param int $depthLeft
161
     * @return OrientatedItem|false
162
     */
163 26
    protected function findBestOrientation(Item $item, OrientatedItem $prevItem = null, Item $nextItem = null, $widthLeft, $lengthLeft, $depthLeft) {
164
165 26
        $orientations = $this->findPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
166
167 26
        if (empty($orientations)) {
168 23
            return false;
169
        }
170
171
        // special casing based on next item
172 26
        if (isset($orientations[0]) && $nextItem == $item && $lengthLeft >= 2 * $item->getLength()) {
173 5
            $this->logger->debug("not rotating based on next item");
174 5
            return $orientations[0]; // XXX this is tied to the ordering from ->findPossibleOrientations()
175
        }
176
177 26
        $orientationFits = [];
178
179
        /** @var OrientatedItem $orientation */
180 26
        foreach ($orientations as $o => $orientation) {
181 26
            $orientationFit = min($widthLeft   - $orientation->getWidth(), $lengthLeft  - $orientation->getLength());
182 26
            $orientationFits[$o] = $orientationFit;
183 26
        }
184
185 26
        if (!empty($orientationFits)) {
186 26
            asort($orientationFits);
187 26
            reset($orientationFits);
188 26
            $bestFit = key($orientationFits);
189 26
            $this->logger->debug("Using orientation #{$bestFit}");
190 26
            return $orientations[$bestFit];
191
        } else {
192
            return false;
193
        }
194
    }
195
196
    /**
197
     * Find all possible orientations for an item
198
     * @param Item $item
199
     * @param OrientatedItem|null $prevItem
200
     * @param int $widthLeft
201
     * @param int $lengthLeft
202
     * @param int $depthLeft
203
     * @return OrientatedItem[]
204
     */
205 26
    protected function findPossibleOrientations(Item $item, OrientatedItem $prevItem = null, $widthLeft, $lengthLeft, $depthLeft) {
206
207 26
        $orientations = [];
208
209
        //Special case items that are the same as what we just packed - keep orientation
210 26
        if ($prevItem && $prevItem->getItem() == $item) {
211 9
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
212 9
        } else {
213
214
            //simple 2D rotation
215 26
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
216 26
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
217
218
            //add 3D rotation if we're allowed
219 26
            if (!$item->getKeepFlat()) {
220 10
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
221 10
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
222 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
223 10
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
224 10
            }
225
        }
226
227
        //remove any that simply don't fit
228 26
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
229 26
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
230 26
        });
231
232
    }
233
234
    /**
235
     * Figure out if we can stack the next item vertically on top of this rather than side by side
236
     * Used when we've packed a tall item, and have just put a shorter one next to it
237
     * @param Item $item
238
     * @param Item $nextItem
239
     * @param $maxStackDepth
240
     * @return bool
241
     */
242 24
    protected function canStackItemInLayer(Item $item, Item $nextItem, $maxStackDepth)
243
    {
244 24
        return $nextItem->getDepth() <= $maxStackDepth &&
245 24
               $nextItem->getWeight() <= $this->remainingWeight &&
246 24
               $nextItem->getWidth() <= $item->getWidth() &&
247 24
               $nextItem->getLength() <= $item->getLength();
248
    }
249
250
    /**
251
     * @param int $layerWidth
252
     * @param int $layerLength
253
     * @param int $layerDepth
254
     * @return bool
255
     */
256 23
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth) {
257 23
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
258
    }
259
}
260