Test Failed
Push — 2.x-dev ( 5e90af...eccc16 )
by Doug
03:16
created

OrientatedItemSorter::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
c 1
b 0
f 0
nc 1
nop 11
dl 0
loc 13
ccs 12
cts 12
cp 1
crap 1
rs 9.9

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
namespace DVDoug\BoxPacker;
8
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerAwareTrait;
11
12
/**
13
 * Figure out best choice of orientations for an item and a given context.
14
 *
15
 * @author Doug Wright
16
 * @internal
17
 */
18
class OrientatedItemSorter implements LoggerAwareInterface
19
{
20
    use LoggerAwareTrait;
21
22
    /**
23
     * @var int[]
24
     */
25
    protected static $lookaheadCache = [];
26
27
    /**
28
     * @var OrientatedItemFactory
29
     */
30
    private $orientatedItemFactory;
31
32
    /**
33
     * @var bool
34
     */
35
    private $singlePassMode;
36
37
    /**
38
     * @var int
39
     */
40
    private $widthLeft;
41
42
    /**
43
     * @var int
44
     */
45
    private $lengthLeft;
46
47
    /**
48
     * @var int
49
     */
50
    private $depthLeft;
51
52
    /**
53
     * @var int
54
     */
55
    private $rowLength;
56
57
    /**
58
     * @var int
59
     */
60
    private $x;
61
62
    /**
63
     * @var int
64
     */
65
    private $y;
66
67
    /**
68
     * @var int
69
     */
70
    private $z;
71
72
    /**
73
     * @var ItemList
74
     */
75
    private $nextItems;
76
77
    /**
78
     * @var PackedItemList
79
     */
80
    private $prevPackedItemList;
81
82 28
    public function __construct(OrientatedItemFactory $factory, $singlePassMode, $widthLeft, $lengthLeft, $depthLeft, ItemList $nextItems, $rowLength, $x, $y, $z, PackedItemList $prevPackedItemList)
83
    {
84 28
        $this->orientatedItemFactory = $factory;
85 28
        $this->singlePassMode = $singlePassMode;
86 28
        $this->widthLeft = $widthLeft;
87 28
        $this->lengthLeft = $lengthLeft;
88 28
        $this->depthLeft = $depthLeft;
89 28
        $this->nextItems = $nextItems;
90 28
        $this->rowLength = $rowLength;
91 28
        $this->x = $x;
92 28
        $this->y = $y;
93 28
        $this->z = $z;
94 28
        $this->prevPackedItemList = $prevPackedItemList;
95 28
    }
96
97 25
    public function __invoke(OrientatedItem $a, OrientatedItem $b)
98
    {
99
        //Prefer exact fits in width/length/depth order
100 25
        $orientationAWidthLeft = $this->widthLeft - $a->getWidth();
101 25
        $orientationBWidthLeft = $this->widthLeft - $b->getWidth();
102 25
        $widthDecider = $this->exactFitDecider($orientationAWidthLeft, $orientationBWidthLeft);
103 25
        if ($widthDecider !== 0) {
104 4
            return $widthDecider;
105
        }
106
107 23
        $orientationALengthLeft = $this->lengthLeft - $a->getLength();
108 23
        $orientationBLengthLeft = $this->lengthLeft - $b->getLength();
109 23
        $lengthDecider = $this->exactFitDecider($orientationALengthLeft, $orientationBLengthLeft);
110 23
        if ($lengthDecider !== 0) {
111 1
            return $lengthDecider;
112
        }
113
114 23
        $orientationADepthLeft = $this->depthLeft - $a->getDepth();
115 23
        $orientationBDepthLeft = $this->depthLeft - $b->getDepth();
116 23
        $depthDecider = $this->exactFitDecider($orientationADepthLeft, $orientationBDepthLeft);
117 23
        if ($depthDecider !== 0) {
118 1
            return $depthDecider;
119
        }
120
121
        // prefer leaving room for next item(s)
122 22
        $followingItemDecider = $this->lookAheadDecider($this->nextItems, $a, $b, $orientationAWidthLeft, $orientationBWidthLeft, $this->widthLeft, $this->lengthLeft, $this->depthLeft, $this->rowLength, $this->x, $this->y, $this->z, $this->prevPackedItemList);
123 10
        if ($followingItemDecider !== 0) {
124 3
            return $followingItemDecider;
125
        }
126
127
        // otherwise prefer leaving minimum possible gap, or the greatest footprint
128 8
        $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
129 8
        $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
130
131 8
        return ($orientationAMinGap - $orientationBMinGap) ?: ($a->getSurfaceFootprint() - $b->getSurfaceFootprint());
132
    }
133
134 22
    private function lookAheadDecider(ItemList $nextItems, OrientatedItem $a, OrientatedItem $b, $orientationAWidthLeft, $orientationBWidthLeft, $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, PackedItemList $prevPackedItemList)
135
    {
136 22
        if ($nextItems->count() === 0) {
137 6
            return 0;
138
        }
139
140 19
        $nextItemFitA = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
141 19
        $nextItemFitB = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
142 19
        if ($nextItemFitA && !$nextItemFitB) {
0 ignored issues
show
introduced by
$nextItemFitB is a non-empty array, thus ! $nextItemFitB is always false.
Loading history...
Bug Best Practice introduced by
The expression $nextItemFitB of type DVDoug\BoxPacker\OrientatedItem[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $nextItemFitA of type DVDoug\BoxPacker\OrientatedItem[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
143
            return -1;
144
        }
145 19
        if ($nextItemFitB && !$nextItemFitA) {
0 ignored issues
show
introduced by
$nextItemFitA is a non-empty array, thus ! $nextItemFitA is always false.
Loading history...
Bug Best Practice introduced by
The expression $nextItemFitA of type DVDoug\BoxPacker\OrientatedItem[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $nextItemFitB of type DVDoug\BoxPacker\OrientatedItem[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
146 3
            return 1;
147
        }
148
149
        // if not an easy either/or, do a partial lookahead
150 17
        $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
151 2
        $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
152
153 2
        return ($additionalPackedB - $additionalPackedA) ?: 0;
154
    }
155
156
    /**
157
     * Approximation of a forward-looking packing.
158
     *
159
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
160
     * purely on fit.
161
     */
162 17
    protected function calculateAdditionalItemsPackedWithThisOrientation(
163
        OrientatedItem $prevItem,
164
        ItemList $nextItems,
165
        $originalWidthLeft,
166
        $originalLengthLeft,
167
        $depthLeft,
168
        $currentRowLengthBeforePacking
169
    ) {
170 17
        if ($this->singlePassMode) {
171 2
            return 0;
172
        }
173
174 17
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
175
176 17
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
177
178
        $cacheKey = $originalWidthLeft .
179 17
            '|' .
180 17
            $originalLengthLeft .
181 17
            '|' .
182 17
            $prevItem->getWidth() .
183 17
            '|' .
184 17
            $prevItem->getLength() .
185 17
            '|' .
186 17
            $currentRowLength .
187 17
            '|'
188 17
            . $depthLeft;
189
190 17
        foreach (clone $itemsToPack as $itemToPack) {
191
            $cacheKey .= '|' .
192 17
                $itemToPack->getWidth() .
193 17
                '|' .
194 17
                $itemToPack->getLength() .
195 17
                '|' .
196 17
                $itemToPack->getDepth() .
197 17
                '|' .
198 17
                $itemToPack->getWeight() .
199 17
                '|' .
200 17
                ($itemToPack->getKeepFlat() ? '1' : '0');
201
        }
202
203 17
        if (!isset(static::$lookaheadCache[$cacheKey])) {
204 17
            $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
205 17
            $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
206 17
            $tempPacker->setSinglePassMode(true);
207 17
            $remainingRowPacked = $tempPacker->pack();
208
209 17
            $itemsToPack->removePackedItems($remainingRowPacked->getItems());
0 ignored issues
show
Bug introduced by
$remainingRowPacked->getItems() of type DVDoug\BoxPacker\ItemList is incompatible with the type DVDoug\BoxPacker\PackedItemList expected by parameter $packedItemList of DVDoug\BoxPacker\ItemList::removePackedItems(). ( Ignorable by Annotation )

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

209
            $itemsToPack->removePackedItems(/** @scrutinizer ignore-type */ $remainingRowPacked->getItems());
Loading history...
210
211
            $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
212
            $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
213
            $tempPacker->setSinglePassMode(true);
214
            $nextRowsPacked = $tempPacker->pack();
215
216
            $itemsToPack->removePackedItems($nextRowsPacked->getItems());
217
218
            $packedCount = $nextItems->count() - $itemsToPack->count();
219
            $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
220
221
            static::$lookaheadCache[$cacheKey] = $packedCount;
222
        }
223
224
        return static::$lookaheadCache[$cacheKey];
225
    }
226
227 25
    private function exactFitDecider($dimensionALeft, $dimensionBLeft)
228
    {
229 25
        if ($dimensionALeft === 0 && $dimensionBLeft > 0) {
230 5
            return -1;
231
        }
232
233 23
        if ($dimensionALeft > 0 && $dimensionBLeft === 0) {
234 2
            return 1;
235
        }
236
237 23
        return 0;
238
    }
239
}
240