Passed
Push — private_sort ( d01cd6...e56742 )
by Doug
06:11
created

OrientatedItemSorter::__invoke()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
c 2
b 0
f 0
nc 5
nop 2
dl 0
loc 35
ccs 22
cts 22
cp 1
crap 6
rs 8.9617
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