Passed
Push — master ( 460ca4...a2f523 )
by Doug
101:17 queued 92:00
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 9
Paths 24

Size

Total Lines 57
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 9

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 9
eloc 28
c 5
b 1
f 0
nc 24
nop 9
dl 0
loc 57
ccs 28
cts 28
cp 1
crap 9
rs 8.0555

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 function array_filter;
12
use function count;
13
use function min;
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerAwareTrait;
16
use Psr\Log\NullLogger;
17
use function reset;
18
use function sort;
19
use function usort;
20
21
/**
22
 * Figure out orientations for an item and a given set of dimensions.
23
 *
24
 * @author Doug Wright
25
 * @internal
26
 */
27
class OrientatedItemFactory implements LoggerAwareInterface
28
{
29
    use LoggerAwareTrait;
30
31
    /** @var Box */
32
    protected $box;
33
34
    /**
35
     * @var OrientatedItem[]
36
     */
37
    protected static $emptyBoxCache = [];
38
39
    /**
40
     * @var int[]
41
     */
42
    protected static $lookaheadCache = [];
43
44 54
    public function __construct(Box $box)
45
    {
46 54
        $this->box = $box;
47 54
        $this->logger = new NullLogger();
48 54
    }
49
50
    /**
51
     * Get the best orientation for an item.
52
     */
53 52
    public function getBestOrientation(
54
        Item $item,
55
        ?OrientatedItem $prevItem,
56
        ItemList $nextItems,
57
        bool $isLastItem,
58
        int $widthLeft,
59
        int $lengthLeft,
60
        int $depthLeft,
61
        int $rowLength,
62
        int $x,
63
        int $y,
64
        int $z,
65
        PackedItemList $prevPackedItemList
66
    ): ?OrientatedItem {
67 52
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
68 52
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations, $isLastItem);
69
70 52
        if (empty($usableOrientations)) {
71 49
            return null;
72
        }
73
74
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList) {
75 28
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
76 28
            $orientationALengthLeft = $lengthLeft - $a->getLength();
77 28
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
78 28
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
79
80 28
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
81 28
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
82
83 28
            if ($orientationAMinGap === 0 && $orientationBMinGap === 0) {
84 12
                return $a->getDepth() <=> $b->getDepth();
85
            }
86 25
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
87 11
                return -1;
88
            }
89 25
            if ($orientationBMinGap === 0) { // prefer B if it leaves no gap
90 16
                return 1;
91
            }
92
93
            // prefer leaving room for next item in current row
94 24
            if ($nextItems->count()) {
95 21
                $nextItemFitA = $this->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
96 21
                $nextItemFitB = $this->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
97 21
                if ($nextItemFitA && !$nextItemFitB) {
98 9
                    return -1;
99
                }
100 19
                if ($nextItemFitB && !$nextItemFitA) {
101 9
                    return 1;
102
                }
103
104
                // if not an easy either/or, do a partial lookahead
105 17
                $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
106 17
                $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
107 17
                if ($additionalPackedA !== $additionalPackedB) {
108 8
                    return $additionalPackedB <=> $additionalPackedA;
109
                }
110
            }
111
            // otherwise prefer leaving minimum possible gap, or the greatest footprint
112 20
            return $orientationAMinGap <=> $orientationBMinGap ?: $a->getSurfaceFootprint() <=> $b->getSurfaceFootprint();
113 52
        });
114
115 52
        $bestFit = reset($usableOrientations);
116 52
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
117
118 52
        return $bestFit;
119
    }
120
121
    /**
122
     * Find all possible orientations for an item.
123
     *
124
     * @return OrientatedItem[]
125
     */
126 53
    public function getPossibleOrientations(
127
        Item $item,
128
        ?OrientatedItem $prevItem,
129
        int $widthLeft,
130
        int $lengthLeft,
131
        int $depthLeft,
132
        int $x,
133
        int $y,
134
        int $z,
135
        PackedItemList $prevPackedItemList
136
    ): array {
137 53
        $orientations = $orientationsDimensions = [];
138
139 53
        $isSame = false;
140 53
        if ($prevItem) {
141 49
            $itemADimensions = [$item->getWidth(), $item->getLength(), $item->getDepth()];
142 49
            $itemBDimensions = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
143 49
            sort($itemADimensions);
144 49
            sort($itemBDimensions);
145 49
            $isSame = ($itemADimensions === $itemBDimensions);
146
        }
147
148
        //Special case items that are the same as what we just packed - keep orientation
149 53
        if ($isSame && $prevItem) {
150 42
            $orientationsDimensions[] = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
151
        } else {
152
            //simple 2D rotation
153 53
            $orientationsDimensions[] = [$item->getWidth(), $item->getLength(), $item->getDepth()];
154 53
            $orientationsDimensions[] = [$item->getLength(), $item->getWidth(), $item->getDepth()];
155
156
            //add 3D rotation if we're allowed
157 53
            if (!$item->getKeepFlat()) {
158 34
                $orientationsDimensions[] = [$item->getWidth(), $item->getDepth(), $item->getLength()];
159 34
                $orientationsDimensions[] = [$item->getLength(), $item->getDepth(), $item->getWidth()];
160 34
                $orientationsDimensions[] = [$item->getDepth(), $item->getWidth(), $item->getLength()];
161 34
                $orientationsDimensions[] = [$item->getDepth(), $item->getLength(), $item->getWidth()];
162
            }
163
        }
164
165
        //remove any that simply don't fit
166 53
        $orientationsDimensions = array_unique($orientationsDimensions, SORT_REGULAR);
167
        $orientationsDimensions = array_filter($orientationsDimensions, static function (array $i) use ($widthLeft, $lengthLeft, $depthLeft) {
168 53
            return $i[0] <= $widthLeft && $i[1] <= $lengthLeft && $i[2] <= $depthLeft;
169 53
        });
170
171 53
        foreach ($orientationsDimensions as $dimensions) {
172 52
            $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
173
        }
174
175 53
        if ($item instanceof ConstrainedPlacementItem) {
176 2
            $box = $this->box;
177
            $orientations = array_filter($orientations, static function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) {
178 2
                return $i->getItem()->canBePacked($box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
1 ignored issue
show
Bug introduced by
The method canBePacked() does not exist on DVDoug\BoxPacker\Item. It seems like you code against a sub-type of DVDoug\BoxPacker\Item such as DVDoug\BoxPacker\ConstrainedPlacementItem or DVDoug\BoxPacker\Test\Co...lacementByCountTestItem or DVDoug\BoxPacker\Test\Co...ementNoStackingTestItem. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

178
                return $i->getItem()->/** @scrutinizer ignore-call */ canBePacked($box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
Loading history...
179 2
            });
180
        }
181
182 53
        return $orientations;
183
    }
184
185
    /**
186
     * @return OrientatedItem[]
187
     */
188 29
    public function getPossibleOrientationsInEmptyBox(Item $item): array
189
    {
190 29
        $cacheKey = $item->getWidth() .
191 29
            '|' .
192 29
            $item->getLength() .
193 29
            '|' .
194 29
            $item->getDepth() .
195 29
            '|' .
196 29
            ($item->getKeepFlat() ? '2D' : '3D') .
197 29
            '|' .
198 29
            $this->box->getInnerWidth() .
199 29
            '|' .
200 29
            $this->box->getInnerLength() .
201 29
            '|' .
202 29
            $this->box->getInnerDepth();
203
204 29
        if (isset(static::$emptyBoxCache[$cacheKey])) {
205 24
            $orientations = static::$emptyBoxCache[$cacheKey];
206
        } else {
207 25
            $orientations = $this->getPossibleOrientations(
208 25
                $item,
209 25
                null,
210 25
                $this->box->getInnerWidth(),
211 25
                $this->box->getInnerLength(),
212 25
                $this->box->getInnerDepth(),
213 25
                0,
214 25
                0,
215 25
                0,
216 25
                new PackedItemList()
217
            );
218 25
            static::$emptyBoxCache[$cacheKey] = $orientations;
219
        }
220
221 29
        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...
222
    }
223
224
    /**
225
     * @param OrientatedItem[] $possibleOrientations
226
     *
227
     * @return OrientatedItem[]
228
     */
229 52
    protected function getUsableOrientations(
230
        Item $item,
231
        $possibleOrientations,
232
        bool $isLastItem
233
    ): array {
234 52
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
235
236
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
237 52
        foreach ($possibleOrientations as $orientation) {
238 52
            if ($orientation->isStable()) {
239 50
                $stableOrientations[] = $orientation;
240
            } else {
241 52
                $unstableOrientations[] = $orientation;
242
            }
243
        }
244
245
        /*
246
         * We prefer to use stable orientations only, but allow unstable ones if either
247
         * the item is the last one left to pack OR
248
         * the item doesn't fit in the box any other way
249
         */
250 52
        if (count($stableOrientations) > 0) {
251 50
            $orientationsToUse = $stableOrientations;
252 49
        } elseif (count($unstableOrientations) > 0) {
253 6
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
254
255 6
            if ($isLastItem || count($stableOrientationsInEmptyBox) === 0) {
256 4
                $orientationsToUse = $unstableOrientations;
257
            }
258
        }
259
260 52
        return $orientationsToUse;
261
    }
262
263
    /**
264
     * Return the orientations for this item if it were to be placed into the box with nothing else.
265
     */
266 6
    protected function getStableOrientationsInEmptyBox(Item $item): array
267
    {
268 6
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
269
270 6
        return array_filter(
271 6
            $orientationsInEmptyBox,
272
            function (OrientatedItem $orientation) {
273 6
                return $orientation->isStable();
274 6
            }
275
        );
276
    }
277
278
    /**
279
     * Approximation of a forward-looking packing.
280
     *
281
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
282
     * purely on fit.
283
     */
284 17
    protected function calculateAdditionalItemsPackedWithThisOrientation(
285
        OrientatedItem $prevItem,
286
        ItemList $nextItems,
287
        int $originalWidthLeft,
288
        int $originalLengthLeft,
289
        int $depthLeft,
290
        int $currentRowLengthBeforePacking
291
    ): int {
292 17
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
293
294 17
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
295
296
        $cacheKey = $originalWidthLeft .
297 17
            '|' .
298 17
            $originalLengthLeft .
299 17
            '|' .
300 17
            $prevItem->getWidth() .
301 17
            '|' .
302 17
            $prevItem->getLength() .
303 17
            '|' .
304 17
            $currentRowLength .
305 17
            '|'
306 17
            . $depthLeft;
307
308
        /** @var Item $itemToPack */
309 17
        foreach ($itemsToPack as $itemToPack) {
310
            $cacheKey .= '|' .
311 17
                $itemToPack->getWidth() .
312 17
                '|' .
313 17
                $itemToPack->getLength() .
314 17
                '|' .
315 17
                $itemToPack->getDepth() .
316 17
                '|' .
317 17
                $itemToPack->getWeight() .
318 17
                '|' .
319 17
                ($itemToPack->getKeepFlat() ? '1' : '0');
320
        }
321
322 17
        if (!isset(static::$lookaheadCache[$cacheKey])) {
323 17
            $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
324 17
            $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
325 17
            $tempPacker->setLookAheadMode(true);
326 17
            $remainingRowPacked = $tempPacker->pack();
327
            /** @var PackedItem $packedItem */
328 17
            foreach ($remainingRowPacked->getItems() as $packedItem) {
329 17
                $itemsToPack->remove($packedItem->getItem());
330
            }
331
332 17
            $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
333 17
            $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
334 17
            $tempPacker->setLookAheadMode(true);
335 17
            $nextRowsPacked = $tempPacker->pack();
336
            /** @var PackedItem $packedItem */
337 17
            foreach ($nextRowsPacked->getItems() as $packedItem) {
338 13
                $itemsToPack->remove($packedItem->getItem());
339
            }
340
341 17
            $packedCount = $nextItems->count() - $itemsToPack->count();
342 17
            $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
343
344 17
            static::$lookaheadCache[$cacheKey] = $packedCount;
345
        }
346
347 17
        return static::$lookaheadCache[$cacheKey];
348
    }
349
}
350