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

OrientatedItemSorter::exactFitDecider()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 5
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 5
rs 9.6111
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