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