Completed
Push — master ( 1d342b...59040c )
by Doug
19:50
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 8
Paths 24

Size

Total Lines 60
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 8.1678

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 8
eloc 29
c 4
b 1
f 0
nc 24
nop 9
dl 0
loc 60
ccs 25
cts 29
cp 0.8621
crap 8.1678
rs 8.2114

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 Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerAwareTrait;
13
use Psr\Log\NullLogger;
14
use function array_filter;
15
use function count;
16
use function min;
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 22
    public function __construct(Box $box)
40
    {
41 22
        $this->box = $box;
42 22
        $this->logger = new NullLogger();
43 22
    }
44
45
    /**
46
     * Get the best orientation for an item.
47
     *
48
     * @param Item                $item
49
     * @param OrientatedItem|null $prevItem
50
     * @param ItemList            $nextItems
51
     * @param bool                $isLastItem
52
     * @param int                 $widthLeft
53
     * @param int                 $lengthLeft
54
     * @param int                 $depthLeft
55
     * @param int                 $rowLength
56
     * @param int                 $x
57
     * @param int                 $y
58
     * @param int                 $z
59
     * @param PackedItemList      $prevPackedItemList
60
     *
61
     * @return OrientatedItem|null
62
     */
63 22
    public function getBestOrientation(
64
        Item $item,
65
        ?OrientatedItem $prevItem,
66
        ItemList $nextItems,
67
        bool $isLastItem,
68
        int $widthLeft,
69
        int $lengthLeft,
70
        int $depthLeft,
71
        int $rowLength,
72
        int $x,
73
        int $y,
74
        int $z,
75
        PackedItemList $prevPackedItemList
76
    ): ?OrientatedItem {
77 22
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
78 22
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations, $isLastItem);
79
80 22
        if (empty($usableOrientations)) {
81 21
            return null;
82
        }
83
84
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList) {
85 9
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
86 9
            $orientationALengthLeft = $lengthLeft - $a->getLength();
87 9
            $orientationADepthLeft = $depthLeft - $a->getDepth();
88 9
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
89 9
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
90 9
            $orientationBDepthLeft = $depthLeft - $b->getDepth();
91
92 9
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
93 9
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
94
95 9
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
96 4
                return -1;
97
            }
98 6
            if ($orientationBMinGap === 0) { // prefer B if it leaves no gap
99 2
                return 1;
100
            }
101
102
            // prefer leaving room for next item in current row
103 5
            if ($nextItems->count()) {
104 4
                $nextItemFitA = count($this->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList));
105 4
                $nextItemFitB = count($this->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList));
106 4
                if ($nextItemFitA && !$nextItemFitB) {
107
                    return -1;
108
                }
109 4
                if ($nextItemFitB && !$nextItemFitA) {
110 2
                    return 1;
111
                }
112
113
                // if not an easy either/or, do a partial lookahead
114 2
                $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
115 2
                $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
116 2
                if ($additionalPackedA !== $additionalPackedB) {
117 1
                    return $additionalPackedB <=> $additionalPackedA;
118
                }
119
            }
120
            // otherwise prefer leaving minimum possible gap, or the greatest footprint
121 2
            return $orientationADepthLeft <=> $orientationBDepthLeft ?: $orientationAMinGap <=> $orientationBMinGap ?: $a->getSurfaceFootprint() <=> $b->getSurfaceFootprint();
122 21
        });
123
124 21
        $bestFit = reset($usableOrientations);
125 21
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
126
127 21
        return $bestFit;
128
    }
129
130
    /**
131
     * Find all possible orientations for an item.
132
     *
133
     * @param Item                $item
134
     * @param OrientatedItem|null $prevItem
135
     * @param int                 $widthLeft
136
     * @param int                 $lengthLeft
137
     * @param int                 $depthLeft
138
     * @param int                 $x
139
     * @param int                 $y
140
     * @param int                 $z
141
     * @param PackedItemList      $prevPackedItemList
142
     *
143
     * @return OrientatedItem[]
144
     */
145 22
    public function getPossibleOrientations(
146
        Item $item,
147
        ?OrientatedItem $prevItem,
148
        int $widthLeft,
149
        int $lengthLeft,
150
        int $depthLeft,
151
        int $x,
152
        int $y,
153
        int $z,
154
        PackedItemList $prevPackedItemList
155
    ): array {
156 22
        $orientations = $orientationsDimensions = [];
157
158 22
        $isSame = false;
159 22
        if ($prevItem) {
160 20
            $itemADimensions = [$item->getWidth(), $item->getLength(), $item->getDepth()];
161 20
            $itemBDimensions = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
162 20
            sort($itemADimensions);
163 20
            sort($itemBDimensions);
164 20
            $isSame = ($itemADimensions === $itemBDimensions);
165
        }
166
167
        //Special case items that are the same as what we just packed - keep orientation
168 22
        if ($isSame) {
169 16
            $orientationsDimensions[] = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
1 ignored issue
show
Bug introduced by
The method getWidth() does not exist on null. ( Ignorable by Annotation )

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

169
            $orientationsDimensions[] = [$prevItem->/** @scrutinizer ignore-call */ getWidth(), $prevItem->getLength(), $prevItem->getDepth()];

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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