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