Passed
Pull Request — master (#344)
by
unknown
05:27 queued 03:46
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 31
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 7.9936

Importance

Changes 10
Bugs 1 Features 0
Metric Value
cc 7
eloc 10
c 10
b 1
f 0
nc 6
nop 9
dl 0
loc 31
ccs 8
cts 11
cp 0.7272
crap 7.9936
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 Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerInterface;
13
use Psr\Log\NullLogger;
14
15
use function array_filter;
16
use function count;
17
use function usort;
18
19
/**
20
 * Figure out orientations for an item and a given set of dimensions.
21
 * @internal
22
 */
23
class OrientatedItemFactory implements LoggerAwareInterface
24
{
25
    protected LoggerInterface $logger;
26
27
    protected Box $box;
28
29
    protected bool $singlePassMode = false;
30
31
    /**
32
     * @var array<string, bool>
33
     */
34
    protected static array $emptyBoxStableItemOrientationCache = [];
35
36 84
    public function __construct(Box $box)
37
    {
38 84
        $this->box = $box;
39 84
        $this->logger = new NullLogger();
40
    }
41
42 84
    public function setLogger(LoggerInterface $logger): void
43
    {
44 84
        $this->logger = $logger;
45
    }
46
47 4
    public function setSinglePassMode(bool $singlePassMode): void
48
    {
49 4
        $this->singlePassMode = $singlePassMode;
50
    }
51
52
    /**
53
     * Get the best orientation for an item.
54
     */
55 84
    public function getBestOrientation(
56
        Item $item,
57
        ?OrientatedItem $prevItem,
58
        ItemList $nextItems,
59
        int $widthLeft,
60
        int $lengthLeft,
61
        int $depthLeft,
62
        int $rowLength,
63
        int $x,
64
        int $y,
65
        int $z,
66
        PackedItemList $prevPackedItemList,
67
        bool $considerStability
68
    ): ?OrientatedItem {
69 84
        $this->logger->debug(
70 84
            "evaluating item {$item->getDescription()} for fit",
71
            [
72 84
                'item' => $item,
73
                'space' => [
74 84
                    'widthLeft' => $widthLeft,
75
                    'lengthLeft' => $lengthLeft,
76
                    'depthLeft' => $depthLeft,
77
                ],
78
            ]
79
        );
80
81 84
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
82 84
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
83
84 84
        if (empty($usableOrientations)) {
85 76
            return null;
86
        }
87
88 82
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, $this->logger);
89 82
        usort($usableOrientations, $sorter);
90
91 82
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
92
93 82
        return $usableOrientations[0];
94
    }
95
96
    /**
97
     * Find all possible orientations for an item.
98
     *
99
     * @return OrientatedItem[]
100
     */
101 84
    public function getPossibleOrientations(
102
        Item $item,
103
        ?OrientatedItem $prevItem,
104
        int $widthLeft,
105
        int $lengthLeft,
106
        int $depthLeft,
107
        int $x,
108
        int $y,
109
        int $z,
110
        PackedItemList $prevPackedItemList
111
    ): array {
112 84
        $permutations = $this->generatePermutations($item, $prevItem);
113
114
        // remove any that simply don't fit
115 84
        $orientations = [];
116 84
        foreach ($permutations as $dimensions) {
117 84
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
118 82
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
119
            }
120
        }
121
122 84
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
123
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList): bool {
124
                /** @var ConstrainedPlacementItem $constrainedItem */
125
                $constrainedItem = $i->getItem();
126
127
                return $constrainedItem->canBePacked(new PackedBox($this->box, $prevPackedItemList), $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
128
            });
129
        }
130
131 84
        return $orientations;
132
    }
133
134
    /**
135
     * @param  OrientatedItem[] $possibleOrientations
136
     * @return OrientatedItem[]
137
     */
138 84
    protected function getUsableOrientations(
139
        Item $item,
140
        array $possibleOrientations
141
    ): array {
142 84
        $stableOrientations = $unstableOrientations = [];
143
144
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
145 84
        foreach ($possibleOrientations as $orientation) {
146 82
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
147 82
                $stableOrientations[] = $orientation;
148
            } else {
149
                $unstableOrientations[] = $orientation;
150
            }
151
        }
152
153
        /*
154
         * We prefer to use stable orientations only, but allow unstable ones if
155
         * the item doesn't fit in the box any other way
156
         */
157 84
        if (count($stableOrientations) > 0) {
158 82
            return $stableOrientations;
159
        }
160
161 76
        if ((count($unstableOrientations) > 0) && !$this->hasStableOrientationsInEmptyBox($item)) {
162
            return $unstableOrientations;
163
        }
164
165 76
        return [];
166
    }
167
168
    /**
169
     * Return the orientations for this item if it were to be placed into the box with nothing else.
170
     */
171
    protected function hasStableOrientationsInEmptyBox(Item $item): bool
172
    {
173
        $cacheKey = $item->getWidth() .
174
            '|' .
175
            $item->getLength() .
176
            '|' .
177
            $item->getDepth() .
178
            '|' .
179
            $item->getAllowedRotation()->name .
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on DVDoug\BoxPacker\Rotation.
Loading history...
180
            '|' .
181
            $this->box->getInnerWidth() .
182
            '|' .
183
            $this->box->getInnerLength() .
184
            '|' .
185
            $this->box->getInnerDepth();
186
187
        if (isset(static::$emptyBoxStableItemOrientationCache[$cacheKey])) {
188
            return static::$emptyBoxStableItemOrientationCache[$cacheKey];
189
        }
190
191
        $orientations = $this->getPossibleOrientations(
192
            $item,
193
            null,
194
            $this->box->getInnerWidth(),
195
            $this->box->getInnerLength(),
196
            $this->box->getInnerDepth(),
197
            0,
198
            0,
199
            0,
200
            new PackedItemList()
201
        );
202
203
        $stableOrientations = array_filter(
204
            $orientations,
205
            static fn (OrientatedItem $orientation) => $orientation->isStable()
206
        );
207
        static::$emptyBoxStableItemOrientationCache[$cacheKey] = count($stableOrientations) > 0;
208
209
        return static::$emptyBoxStableItemOrientationCache[$cacheKey];
210
    }
211
212
    /**
213
     * @return array<array<int>>
214
     */
215 84
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
216
    {
217
        // Special case items that are the same as what we just packed - keep orientation
218 84
        if ($prevItem && $prevItem->isSameDimensions($item)) {
219 62
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
220
        }
221
222 84
        $permutations = [];
223 84
        $w = $item->getWidth();
224 84
        $l = $item->getLength();
225 84
        $d = $item->getDepth();
226
227 84
        $permutations[$w . '|' . $l . '|' . $d] = [$w, $l, $d];
228
229 84
        if ($item->getAllowedRotation() !== Rotation::Never) { // simple 2D rotation
230 84
            $permutations[$l . '|' . $w . '|' . $d] = [$l, $w, $d];
231
        }
232
233 84
        if ($item->getAllowedRotation() === Rotation::BestFit) { // add 3D rotation if we're allowed
234 52
            $permutations[$w . '|' . $d . '|' . $l] = [$w, $d, $l];
235 52
            $permutations[$l . '|' . $d . '|' . $w] = [$l, $d, $w];
236 52
            $permutations[$d . '|' . $w . '|' . $l] = [$d, $w, $l];
237 52
            $permutations[$d . '|' . $l . '|' . $w] = [$d, $l, $w];
238
        }
239
240 84
        return $permutations;
241
    }
242
}
243