Completed
Push — test_jit ( 87859e...ab7b5d )
by Doug
11:43
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
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 58
    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 58
        $this->orientatedItemFactory = $factory;
87 58
        $this->singlePassMode = $singlePassMode;
88 58
        $this->widthLeft = $widthLeft;
89 58
        $this->lengthLeft = $lengthLeft;
90 58
        $this->depthLeft = $depthLeft;
91 58
        $this->nextItems = $nextItems;
92 58
        $this->rowLength = $rowLength;
93 58
        $this->x = $x;
94 58
        $this->y = $y;
95 58
        $this->z = $z;
96 58
        $this->prevPackedItemList = $prevPackedItemList;
97 58
    }
98
99 54
    public function __invoke(OrientatedItem $a, OrientatedItem $b)
100
    {
101
        //Prefer exact fits in width/length/depth order
102 54
        $orientationAWidthLeft = $this->widthLeft - $a->getWidth();
103 54
        $orientationBWidthLeft = $this->widthLeft - $b->getWidth();
104 54
        $widthDecider = $this->exactFitDecider($orientationAWidthLeft, $orientationBWidthLeft);
105 54
        if ($widthDecider !== 0) {
106 11
            return $widthDecider;
107
        }
108
109 52
        $orientationALengthLeft = $this->lengthLeft - $a->getLength();
110 52
        $orientationBLengthLeft = $this->lengthLeft - $b->getLength();
111 52
        $lengthDecider = $this->exactFitDecider($orientationALengthLeft, $orientationBLengthLeft);
112 52
        if ($lengthDecider !== 0) {
113 21
            return $lengthDecider;
114
        }
115
116 52
        $orientationADepthLeft = $this->depthLeft - $a->getDepth();
117 52
        $orientationBDepthLeft = $this->depthLeft - $b->getDepth();
118 52
        $depthDecider = $this->exactFitDecider($orientationADepthLeft, $orientationBDepthLeft);
119 52
        if ($depthDecider !== 0) {
120 5
            return $depthDecider;
121
        }
122
123
        // prefer leaving room for next item(s)
124 51
        $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 51
        if ($followingItemDecider !== 0) {
126 20
            return $followingItemDecider;
127
        }
128
129
        // otherwise prefer leaving minimum possible gap, or the greatest footprint
130 47
        $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
131 47
        $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
132
133 47
        return $orientationAMinGap <=> $orientationBMinGap ?: $a->getSurfaceFootprint() <=> $b->getSurfaceFootprint();
134
    }
135
136 51
    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 51
        if ($nextItems->count() === 0) {
139 33
            return 0;
140
        }
141
142 48
        $nextItemFitA = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
143 48
        $nextItemFitB = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
144 48
        if ($nextItemFitA && !$nextItemFitB) {
145 11
            return -1;
146
        }
147 48
        if ($nextItemFitB && !$nextItemFitA) {
148 14
            return 1;
149
        }
150
151
        // if not an easy either/or, do a partial lookahead
152 46
        $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
153 46
        $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
154
155 46
        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 46
    protected function calculateAdditionalItemsPackedWithThisOrientation(
165
        OrientatedItem $prevItem,
166
        ItemList $nextItems,
167
        int $originalWidthLeft,
168
        int $originalLengthLeft,
169
        int $depthLeft,
170
        int $currentRowLengthBeforePacking
171
    ): int {
172 46
        if ($this->singlePassMode) {
173 22
            return 0;
174
        }
175
176 46
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
177
178 46
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
179
180
        $cacheKey = $originalWidthLeft .
181 46
            '|' .
182 46
            $originalLengthLeft .
183 46
            '|' .
184 46
            $prevItem->getWidth() .
185 46
            '|' .
186 46
            $prevItem->getLength() .
187 46
            '|' .
188 46
            $currentRowLength .
189 46
            '|'
190 46
            . $depthLeft;
191
192 46
        foreach ($itemsToPack as $itemToPack) {
193
            $cacheKey .= '|' .
194 46
                $itemToPack->getWidth() .
195 46
                '|' .
196 46
                $itemToPack->getLength() .
197 46
                '|' .
198 46
                $itemToPack->getDepth() .
199 46
                '|' .
200 46
                $itemToPack->getWeight() .
201 46
                '|' .
202 46
                ($itemToPack->getKeepFlat() ? '1' : '0');
203
        }
204
205 46
        if (!isset(static::$lookaheadCache[$cacheKey])) {
206 43
            $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
207 43
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
208 43
            $tempPacker->setSinglePassMode(true);
209 43
            $remainingRowPacked = $tempPacker->pack();
210
211 43
            $itemsToPack->removePackedItems($remainingRowPacked->getItems());
212
213 43
            $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
214 43
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
215 43
            $tempPacker->setSinglePassMode(true);
216 43
            $nextRowsPacked = $tempPacker->pack();
217
218 43
            $itemsToPack->removePackedItems($nextRowsPacked->getItems());
219
220 43
            $packedCount = $nextItems->count() - $itemsToPack->count();
221 43
            $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
222
223 43
            static::$lookaheadCache[$cacheKey] = $packedCount;
224
        }
225
226 46
        return static::$lookaheadCache[$cacheKey];
227
    }
228
229 54
    private function exactFitDecider(int $dimensionALeft, int $dimensionBLeft)
230
    {
231 54
        if ($dimensionALeft === 0 && $dimensionBLeft > 0) {
232 20
            return -1;
233
        }
234
235 52
        if ($dimensionALeft > 0 && $dimensionBLeft === 0) {
236 19
            return 1;
237
        }
238
239 52
        return 0;
240
    }
241
}
242