Passed
Push — master ( b79dbd...29a054 )
by Doug
08:25 queued 05:38
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 8
Bugs 0 Features 0
Metric Value
eloc 10
dl 0
loc 31
ccs 12
cts 12
cp 1
rs 8.8333
c 8
b 0
f 0
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 bool[]
40
     */
41
    protected static $emptyBoxStableItemOrientationCache = [];
42
43 84
    public function __construct(Box $box)
44
    {
45 84
        $this->box = $box;
46 84
        $this->logger = new NullLogger();
47 84
    }
48
49 68
    public function setSinglePassMode(bool $singlePassMode): void
50
    {
51 68
        $this->singlePassMode = $singlePassMode;
52 68
    }
53
54
    /**
55
     * Get the best orientation for an item.
56
     */
57 76
    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 76
        $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 76
            "evaluating item {$item->getDescription()} for fit",
73
            [
74 76
                'item' => $item,
75
                'space' => [
76 76
                    'widthLeft' => $widthLeft,
77 76
                    'lengthLeft' => $lengthLeft,
78 76
                    'depthLeft' => $depthLeft,
79
                ],
80
            ]
81
        );
82
83 76
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
84 76
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
85
86 76
        if (empty($usableOrientations)) {
87 68
            return null;
88
        }
89
90 76
        $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 76
        usort($usableOrientations, $sorter);
92
93 76
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
94
95 76
        return $usableOrientations[0];
96
    }
97
98
    /**
99
     * Find all possible orientations for an item.
100
     *
101
     * @return OrientatedItem[]
102
     */
103 84
    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 84
        $permutations = $this->generatePermutations($item, $prevItem);
115
116
        //remove any that simply don't fit
117 84
        $orientations = [];
118 84
        foreach ($permutations as $dimensions) {
119 84
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
120 84
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
121
            }
122
        }
123
124 84
        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 84
        return $orientations;
134
    }
135
136
    /**
137
     * @param  OrientatedItem[] $possibleOrientations
138
     * @return OrientatedItem[]
139
     */
140 76
    protected function getUsableOrientations(
141
        Item $item,
142
        array $possibleOrientations
143
    ): array {
144 76
        $stableOrientations = $unstableOrientations = [];
145
146
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
147 76
        foreach ($possibleOrientations as $orientation) {
148 76
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
149 72
                $stableOrientations[] = $orientation;
150
            } else {
151 20
                $unstableOrientations[] = $orientation;
152
            }
153
        }
154
155
        /*
156
         * We prefer to use stable orientations only, but allow unstable ones if
157
         * the item doesn't fit in the box any other way
158
         */
159 76
        if (count($stableOrientations) > 0) {
160 72
            return $stableOrientations;
161
        }
162
163 68
        if ((count($unstableOrientations) > 0) && !$this->hasStableOrientationsInEmptyBox($item)) {
164 14
            return $unstableOrientations;
165
        }
166
167 68
        return [];
168
    }
169
170
    /**
171
     * Return the orientations for this item if it were to be placed into the box with nothing else.
172
     * @return OrientatedItem[]
173
     */
174 20
    protected function hasStableOrientationsInEmptyBox(Item $item): bool
175
    {
176 20
        $cacheKey = $item->getWidth() .
177 20
            '|' .
178 20
            $item->getLength() .
179 20
            '|' .
180 20
            $item->getDepth() .
181 20
            '|' .
182 20
            $item->getAllowedRotations() .
183 20
            '|' .
184 20
            $this->box->getInnerWidth() .
185 20
            '|' .
186 20
            $this->box->getInnerLength() .
187 20
            '|' .
188 20
            $this->box->getInnerDepth();
189
190 20
        if (isset(static::$emptyBoxStableItemOrientationCache[$cacheKey])) {
191 14
            return static::$emptyBoxStableItemOrientationCache[$cacheKey];
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::emptyBoxS...ntationCache[$cacheKey] returns the type boolean which is incompatible with the documented return type DVDoug\BoxPacker\OrientatedItem[].
Loading history...
192
        }
193
194 20
        $orientations = $this->getPossibleOrientations(
195 20
            $item,
196 20
            null,
197 20
            $this->box->getInnerWidth(),
198 20
            $this->box->getInnerLength(),
199 20
            $this->box->getInnerDepth(),
200 20
            0,
201 20
            0,
202 20
            0,
203 20
            new PackedItemList()
204
        );
205
206 20
        $stableOrientations = array_filter(
207 20
            $orientations,
208 20
            static function (OrientatedItem $orientation) {
209 20
                return $orientation->isStable();
210 20
            }
211
        );
212 20
        static::$emptyBoxStableItemOrientationCache[$cacheKey] = count($stableOrientations) > 0;
213
214 20
        return static::$emptyBoxStableItemOrientationCache[$cacheKey];
215
    }
216
217
    /**
218
     * @return array<array<int>>
219
     */
220 84
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
221
    {
222
        //Special case items that are the same as what we just packed - keep orientation
223 84
        if ($prevItem && $prevItem->isSameDimensions($item)) {
224 62
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
225
        }
226
227 84
        $permutations = [];
228 84
        $w = $item->getWidth();
229 84
        $l = $item->getLength();
230 84
        $d = $item->getDepth();
231
232 84
        $permutations[$w . $l . $d] = [$w, $l, $d];
233
234 84
        if ($item->getAllowedRotations() > 1) { //simple 2D rotation
235 82
            $permutations[$l . $w . $d] = [$l, $w, $d];
236
        }
237
238 84
        if ($item->getAllowedRotations() === Item::ROTATION_BEST_FIT) { //add 3D rotation if we're allowed
239 62
            $permutations[$w . $d . $l] = [$w, $d, $l];
240 62
            $permutations[$l . $d . $w] = [$l, $d, $w];
241 62
            $permutations[$d . $w . $l] = [$d, $w, $l];
242 62
            $permutations[$d . $l . $w] = [$d, $l, $w];
243
        }
244
245 84
        return $permutations;
246
    }
247
}
248