Passed
Push — 3.x ( d2d64a...d739e8 )
by Doug
02:33
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

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