Passed
Pull Request — master (#246)
by
unknown
18:54
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 31
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7

Importance

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

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 array<string, array<OrientatedItem>>
40
     */
41
    protected static $emptyBoxCache = [];
42
43 78
    public function __construct(Box $box)
44
    {
45 78
        $this->box = $box;
46 78
        $this->logger = new NullLogger();
47 78
    }
48
49 58
    public function setSinglePassMode(bool $singlePassMode): void
50
    {
51 58
        $this->singlePassMode = $singlePassMode;
52 58
    }
53
54
    /**
55
     * Get the best orientation for an item.
56
     */
57 70
    public function getBestOrientation(
58
        Item $item,
59
        ?OrientatedItem $prevItem,
60
        ItemList $nextItems,
61
        int $widthLeft,
62
        int $lengthLeft,
63
        int $depthLeft,
64
        int $rowLength,
65
        int $x,
66
        int $y,
67
        int $z,
68
        PackedItemList $prevPackedItemList,
69
        bool $considerStability
70
    ): ?OrientatedItem {
71 70
        $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

71
        $this->logger->/** @scrutinizer ignore-call */ 
72
                       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...
72 70
            "evaluating item {$item->getDescription()} for fit",
73
            [
74 70
                'item' => $item,
75
                'space' => [
76 70
                    'widthLeft' => $widthLeft,
77 70
                    'lengthLeft' => $lengthLeft,
78 70
                    'depthLeft' => $depthLeft,
79
                ],
80
            ]
81
        );
82
83 70
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
84 70
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
85
86 70
        if (empty($usableOrientations)) {
87 62
            return null;
88
        }
89
90 70
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, $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\Orienta...emSorter::__construct() 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

90
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, /** @scrutinizer ignore-type */ $this->logger);
Loading history...
91 70
        usort($usableOrientations, $sorter);
92
93 70
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
94
95 70
        return $usableOrientations[0];
96
    }
97
98
    /**
99
     * Find all possible orientations for an item.
100
     *
101
     * @return OrientatedItem[]
102
     */
103 76
    public function getPossibleOrientations(
104
        Item $item,
105
        ?OrientatedItem $prevItem,
106
        int $widthLeft,
107
        int $lengthLeft,
108
        int $depthLeft,
109
        int $x,
110
        int $y,
111
        int $z,
112
        PackedItemList $prevPackedItemList
113
    ): array {
114 76
        $permutations = $this->generatePermutations($item, $prevItem);
115
116
        //remove any that simply don't fit
117 76
        $orientations = [];
118 76
        foreach ($permutations as $dimensions) {
119 76
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
120 76
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
121
            }
122
        }
123
124 76
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
125 6
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
126
                /** @var ConstrainedPlacementItem */
127 6
                $constrainedItem = $i->getItem();
128
129 6
                return $constrainedItem->canBePacked(new PackedBox($this->box, $prevPackedItemList), $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
0 ignored issues
show
Bug introduced by
The method canBePacked() does not exist on DVDoug\BoxPacker\Item. It seems like you code against a sub-type of said class. However, the method does not exist in DVDoug\BoxPacker\Test\TestItem. Are you sure you never get one of those? ( Ignorable by Annotation )

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

129
                return $constrainedItem->/** @scrutinizer ignore-call */ canBePacked(new PackedBox($this->box, $prevPackedItemList), $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
Loading history...
130 6
            });
131
        }
132
133 76
        return $orientations;
134
    }
135
136
    /**
137
     * @return OrientatedItem[]
138
     */
139 54
    public function getPossibleOrientationsInEmptyBox(Item $item): array
140
    {
141 54
        $cacheKey = $item->getWidth() .
142 54
            '|' .
143 54
            $item->getLength() .
144 54
            '|' .
145 54
            $item->getDepth() .
146 54
            '|' .
147 54
            $item->getAllowedRotations() .
148 54
            '|' .
149 54
            $this->box->getInnerWidth() .
150 54
            '|' .
151 54
            $this->box->getInnerLength() .
152 54
            '|' .
153 54
            $this->box->getInnerDepth();
154
155 54
        if (isset(static::$emptyBoxCache[$cacheKey])) {
156 38
            $orientations = static::$emptyBoxCache[$cacheKey];
157
        } else {
158 48
            $orientations = $this->getPossibleOrientations(
159 48
                $item,
160 48
                null,
161 48
                $this->box->getInnerWidth(),
162 48
                $this->box->getInnerLength(),
163 48
                $this->box->getInnerDepth(),
164 48
                0,
165 48
                0,
166 48
                0,
167 48
                new PackedItemList()
168
            );
169 48
            static::$emptyBoxCache[$cacheKey] = $orientations;
170
        }
171
172 54
        return $orientations;
173
    }
174
175
    /**
176
     * @param  OrientatedItem[] $possibleOrientations
177
     * @return OrientatedItem[]
178
     */
179 70
    protected function getUsableOrientations(
180
        Item $item,
181
        array $possibleOrientations
182
    ): array {
183 70
        $stableOrientations = $unstableOrientations = [];
184
185
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
186 70
        foreach ($possibleOrientations as $orientation) {
187 70
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
188 66
                $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 70
        if (count($stableOrientations) > 0) {
199 66
            return $stableOrientations;
200
        }
201
202 62
        if (count($unstableOrientations) > 0) {
203 16
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
204
205 16
            if (count($stableOrientationsInEmptyBox) === 0) {
206 8
                return $unstableOrientations;
207
            }
208
        }
209
210 62
        return [];
211
    }
212
213
    /**
214
     * Return the orientations for this item if it were to be placed into the box with nothing else.
215
     * @return OrientatedItem[]
216
     */
217 16
    protected function getStableOrientationsInEmptyBox(Item $item): array
218
    {
219 16
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
220
221 16
        return array_filter(
222 16
            $orientationsInEmptyBox,
223 16
            function (OrientatedItem $orientation) {
224 16
                return $orientation->isStable();
225 16
            }
226
        );
227
    }
228
229
    /**
230
     * @return array<array<int>>
231
     */
232 76
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
233
    {
234
        //Special case items that are the same as what we just packed - keep orientation
235 76
        if ($prevItem && $prevItem->isSameDimensions($item)) {
236 54
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
237
        }
238
239 76
        $permutations = [];
240 76
        $w = $item->getWidth();
241 76
        $l = $item->getLength();
242 76
        $d = $item->getDepth();
243
244 76
        $permutations[$w . $l . $d] = [$w, $l, $d];
245
246 76
        if ($item->getAllowedRotations() > 1) { //simple 2D rotation
247 74
            $permutations[$l . $w . $d] = [$l, $w, $d];
248
        }
249
250 76
        if ($item->getAllowedRotations() === Item::ROTATION_BEST_FIT) { //add 3D rotation if we're allowed
251 54
            $permutations[$w . $d . $l] = [$w, $d, $l];
252 54
            $permutations[$l . $d . $w] = [$l, $d, $w];
253 54
            $permutations[$d . $w . $l] = [$d, $w, $l];
254 54
            $permutations[$d . $l . $w] = [$d, $l, $w];
255
        }
256
257 76
        return $permutations;
258
    }
259
260
    /**
261
     * Clear the empty box cache. Useful is part of a long running 
262
     * process to prevent memory leaks.
263
     */
264
    public static function clearEmptyBoxCache() : void
265
    {
266
        self::$emptyBoxCache = [];
267
    }
268
}
269