Passed
Push — master ( 5d8812...a1d92b )
by Doug
02:07
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 46
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.392

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 20
c 2
b 1
f 0
nc 6
nop 9
dl 0
loc 46
ccs 16
cts 20
cp 0.8
crap 7.392
rs 8.6666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * Box packing (3D bin packing, knapsack problem).
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace DVDoug\BoxPacker;
10
11
use Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerAwareTrait;
13
use function array_filter;
14
use function count;
15
use function min;
16
use function reset;
17
use function sort;
18
use function usort;
19
20
/**
21
 * Figure out orientations for an item and a given set of dimensions.
22
 *
23
 * @author Doug Wright
24
 * @internal
25
 */
26
class OrientatedItemFactory implements LoggerAwareInterface
27
{
28
    use LoggerAwareTrait;
29
30
    /** @var Box */
31
    protected $box;
32
33
    /**
34
     * @var OrientatedItem[]
35
     */
36
    protected static $emptyBoxCache = [];
37
38 22
    public function __construct(Box $box)
39
    {
40 22
        $this->box = $box;
41 22
    }
42
43
    /**
44
     * Get the best orientation for an item.
45
     *
46
     * @param Item                $item
47
     * @param OrientatedItem|null $prevItem
48
     * @param ItemList            $nextItems
49
     * @param bool                $isLastItem
50
     * @param int                 $widthLeft
51
     * @param int                 $lengthLeft
52
     * @param int                 $depthLeft
53
     * @param int                 $rowLength
54
     * @param int                 $x
55
     * @param int                 $y
56
     * @param int                 $z
57
     * @param PackedItemList      $prevPackedItemList
58
     *
59
     * @return OrientatedItem|null
60
     */
61 21
    public function getBestOrientation(
62
        Item $item,
63
        ?OrientatedItem $prevItem,
64
        ItemList $nextItems,
65
        bool $isLastItem,
66
        int $widthLeft,
67
        int $lengthLeft,
68
        int $depthLeft,
69
        int $rowLength,
70
        int $x,
71
        int $y,
72
        int $z,
73
        PackedItemList $prevPackedItemList
74
    ): ?OrientatedItem {
75 21
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
76 21
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations, $isLastItem);
77
78 21
        if (empty($usableOrientations)) {
79 20
            return null;
80
        }
81
82
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList) {
83 9
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
84 9
            $orientationALengthLeft = $lengthLeft - $a->getLength();
85 9
            $orientationADepthLeft = $depthLeft - $a->getDepth();
86 9
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
87 9
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
88 9
            $orientationBDepthLeft = $depthLeft - $b->getDepth();
89
90 9
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
91 9
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
92
93 9
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
94 4
                return -1;
95
            }
96 6
            if ($orientationBMinGap === 0) { // prefer B if it leaves no gap
97 2
                return 1;
98
            }
99
100
            // prefer leaving room for next item in current row
101 5
            if ($nextItems->count()) {
102 4
                $nextItemFitA = count($this->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList));
103 4
                $nextItemFitB = count($this->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList));
104 4
                if ($nextItemFitA && !$nextItemFitB) {
105
                    return -1;
106
                }
107 4
                if ($nextItemFitB && !$nextItemFitA) {
108 2
                    return 1;
109
                }
110
111
                // if not an easy either/or, do a partial lookahead
112 2
                $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
113 2
                $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
114 2
                if ($additionalPackedA !== $additionalPackedB) {
115 1
                    return $additionalPackedB <=> $additionalPackedA;
116
                }
117
            }
118
            // otherwise prefer leaving minimum possible gap, or the greatest footprint
119 2
            return $orientationADepthLeft <=> $orientationBDepthLeft ?: $orientationAMinGap <=> $orientationBMinGap ?: $a->getSurfaceFootprint() <=> $b->getSurfaceFootprint();
120 21
        });
121
122 21
        $bestFit = reset($usableOrientations);
123 21
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
124
125 21
        return $bestFit;
126
    }
127
128
    /**
129
     * Find all possible orientations for an item.
130
     *
131
     * @param Item                $item
132
     * @param OrientatedItem|null $prevItem
133
     * @param int                 $widthLeft
134
     * @param int                 $lengthLeft
135
     * @param int                 $depthLeft
136
     * @param int                 $x
137
     * @param int                 $y
138
     * @param int                 $z
139
     * @param PackedItemList      $prevPackedItemList
140
     *
141
     * @return OrientatedItem[]
142
     */
143 21
    public function getPossibleOrientations(
144
        Item $item,
145
        ?OrientatedItem $prevItem,
146
        int $widthLeft,
147
        int $lengthLeft,
148
        int $depthLeft,
149
        int $x,
150
        int $y,
151
        int $z,
152
        PackedItemList $prevPackedItemList
153
    ): array {
154 21
        $orientations = [];
155
156
        //Special case items that are the same as what we just packed - keep orientation
157 21
        if ($prevItem && $this->isSameDimensions($prevItem->getItem(), $item)) {
158 16
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
159
        } else {
160
            //simple 2D rotation
161 21
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
162 21
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
163
164
            //add 3D rotation if we're allowed
165 21
            if (!$item->getKeepFlat()) {
166 13
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
167 13
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
168 13
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
169 13
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
170
            }
171
        }
172
173 21
        $orientations = array_unique($orientations);
174
175
        //remove any that simply don't fit
176
        $orientations = array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
177 21
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
178 21
        });
179
180 21
        if ($item instanceof ConstrainedPlacementItem) {
181
            $box = $this->box;
182
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) {
183
                /** @var ConstrainedPlacementItem $constrainedItem */
184
                $constrainedItem = $i->getItem();
185
                return $constrainedItem->canBePacked($box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
186
            });
187
        }
188 21
        return $orientations;
189
    }
190
191
    /**
192
     * @param  Item             $item
193
     * @return OrientatedItem[]
194
     */
195 22
    public function getPossibleOrientationsInEmptyBox(Item $item): array
196
    {
197 22
        $cacheKey = $item->getWidth() .
198 22
            '|' .
199 22
            $item->getLength() .
200 22
            '|' .
201 22
            $item->getDepth() .
202 22
            '|' .
203 22
            ($item->getKeepFlat() ? '2D' : '3D') .
204 22
            '|' .
205 22
            $this->box->getInnerWidth() .
206 22
            '|' .
207 22
            $this->box->getInnerLength() .
208 22
            '|' .
209 22
            $this->box->getInnerDepth();
210
211 22
        if (isset(static::$emptyBoxCache[$cacheKey])) {
212 22
            $orientations = static::$emptyBoxCache[$cacheKey];
213
        } else {
214 20
            $orientations = $this->getPossibleOrientations(
215 20
                $item,
216 20
                null,
217 20
                $this->box->getInnerWidth(),
218 20
                $this->box->getInnerLength(),
219 20
                $this->box->getInnerDepth(),
220 20
                0,
221 20
                0,
222 20
                0,
223 20
                new PackedItemList()
224
            );
225 20
            static::$emptyBoxCache[$cacheKey] = $orientations;
226
        }
227
228 22
        return $orientations;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $orientations could return the type DVDoug\BoxPacker\OrientatedItem which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
229
    }
230
231
    /**
232
     * @param Item             $item
233
     * @param OrientatedItem[] $possibleOrientations
234
     * @param bool             $isLastItem
235
     *
236
     * @return OrientatedItem[]
237
     */
238 21
    protected function getUsableOrientations(
239
        Item $item,
240
        $possibleOrientations,
241
        bool $isLastItem
242
    ): array {
243 21
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
244
245
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
246 21
        foreach ($possibleOrientations as $orientation) {
247 21
            if ($orientation->isStable()) {
248 21
                $stableOrientations[] = $orientation;
249
            } else {
250 21
                $unstableOrientations[] = $orientation;
251
            }
252
        }
253
254
        /*
255
         * We prefer to use stable orientations only, but allow unstable ones if either
256
         * the item is the last one left to pack OR
257
         * the item doesn't fit in the box any other way
258
         */
259 21
        if (count($stableOrientations) > 0) {
260 21
            $orientationsToUse = $stableOrientations;
261 20
        } elseif (count($unstableOrientations) > 0) {
262
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
263
264
            if ($isLastItem || count($stableOrientationsInEmptyBox) === 0) {
265
                $orientationsToUse = $unstableOrientations;
266
            }
267
        }
268
269 21
        return $orientationsToUse;
270
    }
271
272
    /**
273
     * Return the orientations for this item if it were to be placed into the box with nothing else.
274
     *
275
     * @param  Item  $item
276
     * @return array
277
     */
278
    protected function getStableOrientationsInEmptyBox(Item $item): array
279
    {
280
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
281
282
        return array_filter(
283
            $orientationsInEmptyBox,
284
            function (OrientatedItem $orientation) {
285
                return $orientation->isStable();
286
            }
287
        );
288
    }
289
290
    /**
291
     * Compare two items to see if they have same dimensions.
292
     *
293
     * @param Item $itemA
294
     * @param Item $itemB
295
     *
296
     * @return bool
297
     */
298 19
    protected function isSameDimensions(Item $itemA, Item $itemB): bool
299
    {
300 19
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
301 19
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
302 19
        sort($itemADimensions);
303 19
        sort($itemBDimensions);
304
305 19
        return $itemADimensions === $itemBDimensions;
306
    }
307
308
    /**
309
     * Approximation of a forward-looking packing.
310
     *
311
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
312
     * purely on fit.
313
     *
314
     * @param  OrientatedItem $prevItem
315
     * @param  ItemList       $nextItems
316
     * @param  int            $originalWidthLeft
317
     * @param  int            $originalLengthLeft
318
     * @param  int            $depthLeft
319
     * @param  int            $currentRowLengthBeforePacking
320
     * @return int
321
     */
322 2
    protected function calculateAdditionalItemsPackedWithThisOrientation(
323
        OrientatedItem $prevItem,
324
        ItemList $nextItems,
325
        int $originalWidthLeft,
326
        int $originalLengthLeft,
327
        int $depthLeft,
328
        int $currentRowLengthBeforePacking
329
    ): int {
330 2
        $packedCount = 0;
331
332 2
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
333
334 2
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
335
336 2
        $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
337 2
        $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
338 2
        $tempPacker->setLookAheadMode(true);
339 2
        $remainingRowPacked = $tempPacker->pack();
340
        /** @var PackedItem $packedItem */
341 2
        foreach ($remainingRowPacked->getItems() as $packedItem) {
342 2
            $itemsToPack->remove($packedItem->getItem());
343
        }
344
345 2
        $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
346 2
        $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
347 2
        $tempPacker->setLookAheadMode(true);
348 2
        $nextRowsPacked = $tempPacker->pack();
349
        /** @var PackedItem $packedItem */
350 2
        foreach ($nextRowsPacked->getItems() as $packedItem) {
351 1
            $itemsToPack->remove($packedItem->getItem());
352
        }
353
354 2
        $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
355
356 2
        return $nextItems->count() - $itemsToPack->count();
357
    }
358
}
359