Passed
Push — private_sort ( d01cd6 )
by Doug
07:29
created

OrientatedItemSorter   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 217
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 103
c 1
b 0
f 0
dl 0
loc 217
ccs 92
cts 92
cp 1
rs 10
wmc 30

4 Methods

Rating   Name   Duplication   Size   Complexity  
B lookAheadDecider() 0 20 7
C __invoke() 0 41 15
A __construct() 0 13 1
B calculateAdditionalItemsPackedWithThisOrientation() 0 67 7
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