Completed
Push — analysis-z9DYDo ( e56742 )
by Doug
31:32 queued 24:07
created

OrientatedItemSorter::lookAheadDecider()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7

Importance

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

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
declare(strict_types=1);
8
9
namespace DVDoug\BoxPacker;
10
11
use Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerAwareTrait;
13
14
/**
15
 * Figure out best choice of orientations for an item and a given context.
16
 *
17
 * @author Doug Wright
18
 * @internal
19
 */
20
class OrientatedItemSorter implements LoggerAwareInterface
21
{
22
    use LoggerAwareTrait;
23
24
    /**
25
     * @var int[]
26
     */
27
    protected static $lookaheadCache = [];
28
29
    /**
30
     * @var OrientatedItemFactory
31
     */
32
    private $orientatedItemFactory;
33
34
    /**
35
     * @var bool
36
     */
37
    private $singlePassMode;
38
39
    /**
40
     * @var int
41
     */
42
    private $widthLeft;
43
44
    /**
45
     * @var int
46
     */
47
    private $lengthLeft;
48
49
    /**
50
     * @var int
51
     */
52
    private $depthLeft;
53
54
    /**
55
     * @var int
56
     */
57
    private $rowLength;
58
59
    /**
60
     * @var int
61
     */
62
    private $x;
63
64
    /**
65
     * @var int
66
     */
67
    private $y;
68
69
    /**
70
     * @var int
71
     */
72
    private $z;
73
74
    /**
75
     * @var ItemList
76
     */
77
    private $nextItems;
78
79
    /**
80
     * @var PackedItemList
81
     */
82
    private $prevPackedItemList;
83
84 57
    public function __construct(OrientatedItemFactory $factory, bool $singlePassMode, int $widthLeft, int $lengthLeft, int $depthLeft, ItemList $nextItems, int $rowLength, int $x, int $y, int $z, PackedItemList $prevPackedItemList)
85
    {
86 57
        $this->orientatedItemFactory = $factory;
87 57
        $this->singlePassMode = $singlePassMode;
88 57
        $this->widthLeft = $widthLeft;
89 57
        $this->lengthLeft = $lengthLeft;
90 57
        $this->depthLeft = $depthLeft;
91 57
        $this->nextItems = $nextItems;
92 57
        $this->rowLength = $rowLength;
93 57
        $this->x = $x;
94 57
        $this->y = $y;
95 57
        $this->z = $z;
96 57
        $this->prevPackedItemList = $prevPackedItemList;
97 57
    }
98
99 53
    public function __invoke(OrientatedItem $a, OrientatedItem $b)
100
    {
101
        //Prefer exact fits in width/length/depth order
102 53
        $orientationAWidthLeft = $this->widthLeft - $a->getWidth();
103 53
        $orientationBWidthLeft = $this->widthLeft - $b->getWidth();
104 53
        $widthDecider = $this->exactFitDecider($orientationAWidthLeft, $orientationBWidthLeft);
105 53
        if ($widthDecider !== 0) {
106 10
            return $widthDecider;
107
        }
108
109 51
        $orientationALengthLeft = $this->lengthLeft - $a->getLength();
110 51
        $orientationBLengthLeft = $this->lengthLeft - $b->getLength();
111 51
        $lengthDecider = $this->exactFitDecider($orientationALengthLeft, $orientationBLengthLeft);
112 51
        if ($lengthDecider !== 0) {
113 20
            return $lengthDecider;
114
        }
115
116 51
        $orientationADepthLeft = $this->depthLeft - $a->getDepth();
117 51
        $orientationBDepthLeft = $this->depthLeft - $b->getDepth();
118 51
        $depthDecider = $this->exactFitDecider($orientationADepthLeft, $orientationBDepthLeft);
119 51
        if ($depthDecider !== 0) {
120 5
            return $depthDecider;
121
        }
122
123
        // prefer leaving room for next item(s)
124 50
        $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);
125 50
        if ($followingItemDecider !== 0) {
126 19
            return $followingItemDecider;
127
        }
128
129
        // otherwise prefer leaving minimum possible gap, or the greatest footprint
130 46
        $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
131 46
        $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
132
133 46
        return $orientationAMinGap <=> $orientationBMinGap ?: $a->getSurfaceFootprint() <=> $b->getSurfaceFootprint();
134
    }
135
136 50
    private function lookAheadDecider(ItemList $nextItems, OrientatedItem $a, OrientatedItem $b, int $orientationAWidthLeft, int $orientationBWidthLeft, int $widthLeft, int $lengthLeft, int $depthLeft, int $rowLength, int $x, int $y, int $z, PackedItemList $prevPackedItemList): int
137
    {
138 50
        if ($nextItems->count() === 0) {
139 32
            return 0;
140
        }
141
142 47
        $nextItemFitA = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
143 47
        $nextItemFitB = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
144 47
        if ($nextItemFitA && !$nextItemFitB) {
145 10
            return -1;
146
        }
147 47
        if ($nextItemFitB && !$nextItemFitA) {
148 13
            return 1;
149
        }
150
151
        // if not an easy either/or, do a partial lookahead
152 45
        $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
153 45
        $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
154
155 45
        return $additionalPackedB <=> $additionalPackedA ?: 0;
156
    }
157
158
    /**
159
     * Approximation of a forward-looking packing.
160
     *
161
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
162
     * purely on fit.
163
     */
164 45
    protected function calculateAdditionalItemsPackedWithThisOrientation(
165
        OrientatedItem $prevItem,
166
        ItemList $nextItems,
167
        int $originalWidthLeft,
168
        int $originalLengthLeft,
169
        int $depthLeft,
170
        int $currentRowLengthBeforePacking
171
    ): int {
172 45
        if ($this->singlePassMode) {
173 21
            return 0;
174
        }
175
176 45
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
177
178 45
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
179
180
        $cacheKey = $originalWidthLeft .
181 45
            '|' .
182 45
            $originalLengthLeft .
183 45
            '|' .
184 45
            $prevItem->getWidth() .
185 45
            '|' .
186 45
            $prevItem->getLength() .
187 45
            '|' .
188 45
            $currentRowLength .
189 45
            '|'
190 45
            . $depthLeft;
191
192 45
        foreach ($itemsToPack as $itemToPack) {
193
            $cacheKey .= '|' .
194 45
                $itemToPack->getWidth() .
195 45
                '|' .
196 45
                $itemToPack->getLength() .
197 45
                '|' .
198 45
                $itemToPack->getDepth() .
199 45
                '|' .
200 45
                $itemToPack->getWeight() .
201 45
                '|' .
202 45
                ($itemToPack->getKeepFlat() ? '1' : '0');
203
        }
204
205 45
        if (!isset(static::$lookaheadCache[$cacheKey])) {
206 42
            $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
207 42
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
208 42
            $tempPacker->setSinglePassMode(true);
209 42
            $remainingRowPacked = $tempPacker->pack();
210
211 42
            foreach ($remainingRowPacked->getItems() as $packedItem) {
212 28
                $itemsToPack->remove($packedItem->getItem());
213
            }
214
215 42
            $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
216 42
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
217 42
            $tempPacker->setSinglePassMode(true);
218 42
            $nextRowsPacked = $tempPacker->pack();
219
220 42
            foreach ($nextRowsPacked->getItems() as $packedItem) {
221 24
                $itemsToPack->remove($packedItem->getItem());
222
            }
223
224 42
            $packedCount = $nextItems->count() - $itemsToPack->count();
225 42
            $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
226
227 42
            static::$lookaheadCache[$cacheKey] = $packedCount;
228
        }
229
230 45
        return static::$lookaheadCache[$cacheKey];
231
    }
232
233 53
    private function exactFitDecider(int $dimensionALeft, int $dimensionBLeft)
234
    {
235 53
        if ($dimensionALeft === 0 && $dimensionBLeft > 0) {
236 19
            return -1;
237
        }
238
239 51
        if ($dimensionALeft > 0 && $dimensionBLeft === 0) {
240 18
            return 1;
241
        }
242
243 51
        return 0;
244
    }
245
}
246