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, |
||
0 ignored issues
–
show
|
|||
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 /**
* @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. ![]() |
|||
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 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.