Passed
Push — private_sort ( d01cd6 )
by Doug
07:29
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 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
        if ($orientationAWidthLeft === 0 && $orientationBWidthLeft > 0) {
105 8
            return -1;
106
        }
107 51
        if ($orientationBWidthLeft === 0 && $orientationAWidthLeft > 0) {
108 6
            return 1;
109
        }
110
111 51
        $orientationALengthLeft = $this->lengthLeft - $a->getLength();
112 51
        $orientationBLengthLeft = $this->lengthLeft - $b->getLength();
113 51
        if ($orientationALengthLeft === 0 && $orientationBLengthLeft > 0) {
114 15
            return -1;
115
        }
116 51
        if ($orientationBLengthLeft === 0 && $orientationALengthLeft > 0) {
117 16
            return 1;
118
        }
119
120 51
        $orientationADepthLeft = $this->depthLeft - $a->getDepth();
121 51
        $orientationBDepthLeft = $this->depthLeft - $b->getDepth();
122 51
        if ($orientationADepthLeft === 0 && $orientationBDepthLeft > 0) {
123 5
            return -1;
124
        }
125 51
        if ($orientationBDepthLeft === 0 && $orientationADepthLeft > 0) {
126 4
            return 1;
127
        }
128
129
        // prefer leaving room for next item(s)
130 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);
131 50
        if ($followingItemDecider !== 0) {
132 19
            return $followingItemDecider;
133
        }
134
135
        // otherwise prefer leaving minimum possible gap, or the greatest footprint
136 46
        $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
137 46
        $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
138
139 46
        return $orientationAMinGap <=> $orientationBMinGap ?: $a->getSurfaceFootprint() <=> $b->getSurfaceFootprint();
140
    }
141
142 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
143
    {
144 50
        if ($nextItems->count() === 0) {
145 32
            return 0;
146
        }
147
148 47
        $nextItemFitA = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
149 47
        $nextItemFitB = $this->orientatedItemFactory->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
150 47
        if ($nextItemFitA && !$nextItemFitB) {
151 10
            return -1;
152
        }
153 47
        if ($nextItemFitB && !$nextItemFitA) {
154 13
            return 1;
155
        }
156
157
        // if not an easy either/or, do a partial lookahead
158 45
        $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
159 45
        $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
160
161 45
        return $additionalPackedB <=> $additionalPackedA ?: 0;
162
    }
163
164
    /**
165
     * Approximation of a forward-looking packing.
166
     *
167
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
168
     * purely on fit.
169
     */
170 45
    protected function calculateAdditionalItemsPackedWithThisOrientation(
171
        OrientatedItem $prevItem,
172
        ItemList $nextItems,
173
        int $originalWidthLeft,
174
        int $originalLengthLeft,
175
        int $depthLeft,
176
        int $currentRowLengthBeforePacking
177
    ): int {
178 45
        if ($this->singlePassMode) {
179 21
            return 0;
180
        }
181
182 45
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
183
184 45
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
185
186
        $cacheKey = $originalWidthLeft .
187 45
            '|' .
188 45
            $originalLengthLeft .
189 45
            '|' .
190 45
            $prevItem->getWidth() .
191 45
            '|' .
192 45
            $prevItem->getLength() .
193 45
            '|' .
194 45
            $currentRowLength .
195 45
            '|'
196 45
            . $depthLeft;
197
198 45
        foreach ($itemsToPack as $itemToPack) {
199
            $cacheKey .= '|' .
200 45
                $itemToPack->getWidth() .
201 45
                '|' .
202 45
                $itemToPack->getLength() .
203 45
                '|' .
204 45
                $itemToPack->getDepth() .
205 45
                '|' .
206 45
                $itemToPack->getWeight() .
207 45
                '|' .
208 45
                ($itemToPack->getKeepFlat() ? '1' : '0');
209
        }
210
211 45
        if (!isset(static::$lookaheadCache[$cacheKey])) {
212 42
            $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
213 42
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
214 42
            $tempPacker->setSinglePassMode(true);
215 42
            $remainingRowPacked = $tempPacker->pack();
216
217 42
            foreach ($remainingRowPacked->getItems() as $packedItem) {
218 28
                $itemsToPack->remove($packedItem->getItem());
219
            }
220
221 42
            $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
222 42
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
223 42
            $tempPacker->setSinglePassMode(true);
224 42
            $nextRowsPacked = $tempPacker->pack();
225
226 42
            foreach ($nextRowsPacked->getItems() as $packedItem) {
227 24
                $itemsToPack->remove($packedItem->getItem());
228
            }
229
230 42
            $packedCount = $nextItems->count() - $itemsToPack->count();
231 42
            $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
232
233 42
            static::$lookaheadCache[$cacheKey] = $packedCount;
234
        }
235
236 45
        return static::$lookaheadCache[$cacheKey];
237
    }
238
}
239