Test Failed
Push — master ( 09012b...794d70 )
by Doug
04:43
created

calculateAdditionalItemsPackedWithThisOrientation()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 63
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 41
dl 0
loc 63
ccs 40
cts 40
cp 1
c 1
b 0
f 0
rs 9.264
cc 4
nc 5
nop 6
crap 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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