Passed
Push — 3.x ( ec426a...33350c )
by Doug
01:45
created

hasPossibleOrientationsInEmptyBox()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 34
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 24
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 34
ccs 0
cts 28
cp 0
crap 12
rs 9.536
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 function array_filter;
12
use function count;
13
use Psr\Log\LoggerAwareInterface;
14
use Psr\Log\LoggerAwareTrait;
15
use Psr\Log\NullLogger;
16
use function usort;
17
18
/**
19
 * Figure out orientations for an item and a given set of dimensions.
20
 *
21
 * @author Doug Wright
22
 * @internal
23
 */
24
class OrientatedItemFactory implements LoggerAwareInterface
25
{
26
    use LoggerAwareTrait;
27
28
    /** @var Box */
29
    protected $box;
30
31
    /**
32
     * Whether the packer is in single-pass mode.
33
     *
34
     * @var bool
35
     */
36
    protected $singlePassMode = false;
37
38
    /**
39
     * @var bool[]
40
     */
41
    protected static $emptyBoxItemOrientationCache = [];
42
43
    /**
44
     * @var bool[]
45
     */
46
    protected static $emptyBoxStableItemOrientationCache = [];
47
48 72
    public function __construct(Box $box)
49
    {
50 72
        $this->box = $box;
51 72
        $this->logger = new NullLogger();
52 72
    }
53
54 62
    public function setSinglePassMode(bool $singlePassMode): void
55
    {
56 62
        $this->singlePassMode = $singlePassMode;
57 62
    }
58
59
    /**
60
     * Get the best orientation for an item.
61
     */
62 72
    public function getBestOrientation(
63
        Item $item,
64
        ?OrientatedItem $prevItem,
65
        ItemList $nextItems,
66
        int $widthLeft,
67
        int $lengthLeft,
68
        int $depthLeft,
69
        int $rowLength,
70
        int $x,
71
        int $y,
72
        int $z,
73
        PackedItemList $prevPackedItemList,
74
        bool $considerStability
75
    ): ?OrientatedItem {
76 72
        $this->logger->debug(
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

76
        $this->logger->/** @scrutinizer ignore-call */ 
77
                       debug(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
77 72
            "evaluating item {$item->getDescription()} for fit",
78
            [
79 72
                'item' => $item,
80
                'space' => [
81 72
                    'widthLeft' => $widthLeft,
82 72
                    'lengthLeft' => $lengthLeft,
83 72
                    'depthLeft' => $depthLeft,
84
                ],
85
            ]
86
        );
87
88 72
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
89 72
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
90
91 72
        if (empty($usableOrientations)) {
92 62
            return null;
93
        }
94
95 72
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList);
96 72
        $sorter->setLogger($this->logger);
0 ignored issues
show
Bug introduced by
It seems like $this->logger can also be of type null; however, parameter $logger of DVDoug\BoxPacker\OrientatedItemSorter::setLogger() does only seem to accept Psr\Log\LoggerInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

96
        $sorter->setLogger(/** @scrutinizer ignore-type */ $this->logger);
Loading history...
97 72
        usort($usableOrientations, $sorter);
98
99 72
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
100
101 72
        return $usableOrientations[0];
102
    }
103
104
    /**
105
     * Find all possible orientations for an item.
106
     *
107
     * @return OrientatedItem[]
108
     */
109 72
    public function getPossibleOrientations(
110
        Item $item,
111
        ?OrientatedItem $prevItem,
112
        int $widthLeft,
113
        int $lengthLeft,
114
        int $depthLeft,
115
        int $x,
116
        int $y,
117
        int $z,
118
        PackedItemList $prevPackedItemList
119
    ): array {
120 72
        $permutations = $this->generatePermutations($item, $prevItem);
121
122
        //remove any that simply don't fit
123 72
        $orientations = [];
124 72
        foreach ($permutations as $dimensions) {
125 72
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
126 72
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
127
            }
128
        }
129
130 72
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
131 3
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
132 6
                return $i->getItem()->canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
133 6
            });
134
        }
135
136 72
        return $orientations;
137
    }
138
139
    public function hasPossibleOrientationsInEmptyBox(Item $item): bool
140
    {
141
        $cacheKey = $item->getWidth() .
142
            '|' .
143
            $item->getLength() .
144
            '|' .
145
            $item->getDepth() .
146
            '|' .
147
            ($item->getKeepFlat() ? '2D' : '3D') .
148
            '|' .
149
            $this->box->getInnerWidth() .
150
            '|' .
151
            $this->box->getInnerLength() .
152
            '|' .
153
            $this->box->getInnerDepth();
154
155
        if (isset(static::$emptyBoxItemOrientationCache[$cacheKey])) {
156
            return static::$emptyBoxItemOrientationCache[$cacheKey];
157
        }
158
159
        $orientations = $this->getPossibleOrientations(
160
            $item,
161
            null,
162
            $this->box->getInnerWidth(),
163
            $this->box->getInnerLength(),
164
            $this->box->getInnerDepth(),
165
            0,
166
            0,
167
            0,
168
            new PackedItemList()
169
        );
170
        static::$emptyBoxItemOrientationCache[$cacheKey] = count($orientations) > 0;
171
172
        return static::$emptyBoxItemOrientationCache[$cacheKey];
173
    }
174
175
    /**
176
     * @param  OrientatedItem[] $possibleOrientations
177
     * @return OrientatedItem[]
178
     */
179 72
    protected function getUsableOrientations(
180
        Item $item,
181
        array $possibleOrientations
182
    ): array {
183 72
        $stableOrientations = $unstableOrientations = [];
184
185
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
186 72
        foreach ($possibleOrientations as $orientation) {
187 72
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
188 68
                $stableOrientations[] = $orientation;
189
            } else {
190 18
                $unstableOrientations[] = $orientation;
191
            }
192
        }
193
194
        /*
195
         * We prefer to use stable orientations only, but allow unstable ones if
196
         * the item doesn't fit in the box any other way
197
         */
198 72
        if (count($stableOrientations) > 0) {
199 68
            return $stableOrientations;
200
        }
201
202 62
        if ((count($unstableOrientations) > 0) && !$this->hasStableOrientationsInEmptyBox($item)) {
203 12
            return $unstableOrientations;
204
        }
205
206 62
        return [];
207
    }
208
209
    /**
210
     * Return the orientations for this item if it were to be placed into the box with nothing else.
211
     */
212 18
    protected function hasStableOrientationsInEmptyBox(Item $item): bool
213
    {
214 18
        $cacheKey = $item->getWidth() .
215 18
            '|' .
216 18
            $item->getLength() .
217 18
            '|' .
218 18
            $item->getDepth() .
219 18
            '|' .
220 18
            ($item->getKeepFlat() ? '2D' : '3D') .
221 18
            '|' .
222 18
            $this->box->getInnerWidth() .
223 18
            '|' .
224 18
            $this->box->getInnerLength() .
225 18
            '|' .
226 18
            $this->box->getInnerDepth();
227
228 18
        if (isset(static::$emptyBoxStableItemOrientationCache[$cacheKey])) {
229 12
            return static::$emptyBoxStableItemOrientationCache[$cacheKey];
230
        }
231
232 18
        $orientations = $this->getPossibleOrientations(
233 18
            $item,
234 18
            null,
235 18
            $this->box->getInnerWidth(),
236 18
            $this->box->getInnerLength(),
237 18
            $this->box->getInnerDepth(),
238 18
            0,
239 18
            0,
240 18
            0,
241 18
            new PackedItemList()
242
        );
243
244 18
        $stableOrientations = array_filter(
245 18
            $orientations,
246 9
            static function (OrientatedItem $orientation) {
247 18
                return $orientation->isStable();
248 18
            }
249
        );
250 18
        static::$emptyBoxStableItemOrientationCache[$cacheKey] = count($stableOrientations) > 0;
251
252 18
        return static::$emptyBoxStableItemOrientationCache[$cacheKey];
253
    }
254
255 72
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
256
    {
257
        //Special case items that are the same as what we just packed - keep orientation
258 72
        if ($prevItem && $prevItem->isSameDimensions($item)) {
259 58
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
260
        }
261
262 72
        $permutations = [];
263 72
        $w = $item->getWidth();
264 72
        $l = $item->getLength();
265 72
        $d = $item->getDepth();
266
267
        //simple 2D rotation
268 72
        $permutations[$w . $l . $d] = [$w, $l, $d];
269 72
        $permutations[$l . $w . $d] = [$l, $w, $d];
270
271
        //add 3D rotation if we're allowed
272 72
        if (!$item->getKeepFlat()) {
273 52
            $permutations[$w . $d . $l] = [$w, $d, $l];
274 52
            $permutations[$l . $d . $w] = [$l, $d, $w];
275 52
            $permutations[$d . $w . $l] = [$d, $w, $l];
276 52
            $permutations[$d . $l . $w] = [$d, $l, $w];
277
        }
278
279 72
        return $permutations;
280
    }
281
}
282