Completed
Push — issue_187 ( 8952f9...e99851 )
by Doug
12:21
created

OrientatedItemFactory::getBestOrientation()   C

Complexity

Conditions 13
Paths 2

Size

Total Lines 66
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 13

Importance

Changes 10
Bugs 3 Features 0
Metric Value
cc 13
eloc 32
c 10
b 3
f 0
nc 2
nop 12
dl 0
loc 66
ccs 33
cts 33
cp 1
crap 13
rs 6.6166

How to fix   Long Method    Complexity    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 23
    public function __construct(Box $box)
45
    {
46 23
        $this->box = $box;
47 23
        $this->logger = new NullLogger();
48 23
    }
49
50
    /**
51
     * Get the best orientation for an item.
52
     */
53 22
    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 22
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
68 22
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations, $isLastItem);
69
70 22
        if (empty($usableOrientations)) {
71 21
            return null;
72
        }
73
74
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList) {
75 10
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
76 10
            $orientationALengthLeft = $lengthLeft - $a->getLength();
77 10
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
78 10
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
79
80 10
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
81 10
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
82
83 10
            if ($orientationAMinGap === 0 && $orientationBMinGap === 0) {
84 4
                return $a->getDepth() <=> $b->getDepth();
85
            }
86 7
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
87 2
                return -1;
88
            }
89 7
            if ($orientationBMinGap === 0) { // prefer B if it leaves no gap
90 3
                return 1;
91
            }
92
93
            // prefer leaving room for next item in current row
94 6
            if ($nextItems->count()) {
95 5
                $nextItemFitA = $this->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
96 5
                $nextItemFitB = $this->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
97 5
                if ($nextItemFitA && !$nextItemFitB) {
98 1
                    return -1;
99
                }
100 5
                if ($nextItemFitB && !$nextItemFitA) {
101 3
                    return 1;
102
                }
103
104
                // if not an easy either/or, do a partial lookahead
105 3
                $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
106 3
                $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
107 3
                if ($additionalPackedA !== $additionalPackedB) {
108 2
                    return $additionalPackedB <=> $additionalPackedA;
109
                }
110
            }
111
            // otherwise prefer leaving minimum possible gap, or the greatest footprint
112 3
            return $orientationAMinGap <=> $orientationBMinGap ?: $a->getSurfaceFootprint() <=> $b->getSurfaceFootprint();
113 22
        });
114
115 22
        $bestFit = reset($usableOrientations);
116 22
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
117
118 22
        return $bestFit;
119
    }
120
121
    /**
122
     * Find all possible orientations for an item.
123
     *
124
     * @return OrientatedItem[]
125
     */
126 22
    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 22
        $orientations = $orientationsDimensions = [];
138
139 22
        $isSame = false;
140 22
        if ($prevItem) {
141 21
            if ($item === $prevItem->getItem()) {
142 14
                $isSame = true;
143
            } else {
144 14
                $itemADimensions = [$item->getWidth(), $item->getLength(), $item->getDepth()];
145 14
                $itemBDimensions = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
146 14
                sort($itemADimensions);
147 14
                sort($itemBDimensions);
148 14
                $isSame = ($itemADimensions === $itemBDimensions);
149
            }
150
        }
151
152
        //Special case items that are the same as what we just packed - keep orientation
153 22
        if ($isSame && $prevItem) {
154 17
            $orientationsDimensions[] = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
155
        } else {
156
            //simple 2D rotation
157 22
            $orientationsDimensions[] = [$item->getWidth(), $item->getLength(), $item->getDepth()];
158 22
            $orientationsDimensions[] = [$item->getLength(), $item->getWidth(), $item->getDepth()];
159
160
            //add 3D rotation if we're allowed
161 22
            if (!$item->getKeepFlat()) {
162 14
                $orientationsDimensions[] = [$item->getWidth(), $item->getDepth(), $item->getLength()];
163 14
                $orientationsDimensions[] = [$item->getLength(), $item->getDepth(), $item->getWidth()];
164 14
                $orientationsDimensions[] = [$item->getDepth(), $item->getWidth(), $item->getLength()];
165 14
                $orientationsDimensions[] = [$item->getDepth(), $item->getLength(), $item->getWidth()];
166
            }
167
        }
168
169
        //remove any that simply don't fit
170 22
        $orientationsDimensions = array_unique($orientationsDimensions, SORT_REGULAR);
171
        $orientationsDimensions = array_filter($orientationsDimensions, static function (array $i) use ($widthLeft, $lengthLeft, $depthLeft) {
172 22
            return $i[0] <= $widthLeft && $i[1] <= $lengthLeft && $i[2] <= $depthLeft;
173 22
        });
174
175 22
        foreach ($orientationsDimensions as $dimensions) {
176 22
            $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
177
        }
178
179 22
        if ($item instanceof ConstrainedPlacementItem) {
180
            $box = $this->box;
181
            $orientations = array_filter($orientations, static function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) {
182
                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

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