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

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7

Importance

Changes 10
Bugs 1 Features 0
Metric Value
cc 7
eloc 9
c 10
b 1
f 0
nc 6
nop 9
dl 0
loc 28
ccs 11
cts 11
cp 1
crap 7
rs 8.8333

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 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