Passed
Push — master ( 73ddcb...629397 )
by Doug
02:34
created

OrientatedItemFactory::getBestOrientation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 39
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 2

Importance

Changes 14
Bugs 3 Features 0
Metric Value
cc 2
eloc 16
c 14
b 3
f 0
nc 2
nop 11
dl 0
loc 39
ccs 16
cts 16
cp 1
crap 2
rs 9.7333

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 78
    public function __construct(Box $box)
44
    {
45 78
        $this->box = $box;
46 78
        $this->logger = new NullLogger();
47 78
    }
48
49 53
    public function setSinglePassMode(bool $singlePassMode): void
50
    {
51 53
        $this->singlePassMode = $singlePassMode;
52 53
    }
53
54
    /**
55
     * Get the best orientation for an item.
56
     */
57 73
    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
    ): ?OrientatedItem {
70 73
        $this->logger->debug(
71 73
            "evaluating item {$item->getDescription()} for fit",
72
            [
73 73
                'item' => $item,
74
                'space' => [
75 73
                    'widthLeft' => $widthLeft,
76 73
                    'lengthLeft' => $lengthLeft,
77 73
                    'depthLeft' => $depthLeft,
78
                ],
79
            ]
80
        );
81
82 73
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
83 73
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations);
84
85 73
        if (empty($usableOrientations)) {
86 66
            return null;
87
        }
88
89 73
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList);
90 73
        $sorter->setLogger($this->logger);
91 73
        usort($usableOrientations, $sorter);
92
93 73
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
94
95 73
        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 3
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
126 3
                return $i->getItem()->canBePacked($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

126
                return $i->getItem()->/** @scrutinizer ignore-call */ canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
Loading history...
127 3
            });
128
        }
129
130 76
        return $orientations;
131
    }
132
133
    /**
134
     * @return OrientatedItem[]
135
     */
136 37
    public function getPossibleOrientationsInEmptyBox(Item $item): array
137
    {
138 37
        $cacheKey = $item->getWidth() .
139 37
            '|' .
140 37
            $item->getLength() .
141 37
            '|' .
142 37
            $item->getDepth() .
143 37
            '|' .
144 37
            $item->getAllowedRotations() .
145 37
            '|' .
146 37
            $this->box->getInnerWidth() .
147 37
            '|' .
148 37
            $this->box->getInnerLength() .
149 37
            '|' .
150 37
            $this->box->getInnerDepth();
151
152 37
        if (isset(static::$emptyBoxCache[$cacheKey])) {
153 28
            $orientations = static::$emptyBoxCache[$cacheKey];
154
        } else {
155 28
            $orientations = $this->getPossibleOrientations(
156 28
                $item,
157 28
                null,
158 28
                $this->box->getInnerWidth(),
159 28
                $this->box->getInnerLength(),
160 28
                $this->box->getInnerDepth(),
161 28
                0,
162 28
                0,
163 28
                0,
164 28
                new PackedItemList()
165
            );
166 28
            static::$emptyBoxCache[$cacheKey] = $orientations;
167
        }
168
169 37
        return $orientations;
170
    }
171
172
    /**
173
     * @param  OrientatedItem[] $possibleOrientations
174
     * @return OrientatedItem[]
175
     */
176 73
    protected function getUsableOrientations(
177
        Item $item,
178
        array $possibleOrientations
179
    ): array {
180 73
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
181
182
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
183 73
        foreach ($possibleOrientations as $orientation) {
184 73
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
185 71
                $stableOrientations[] = $orientation;
186
            } else {
187 7
                $unstableOrientations[] = $orientation;
188
            }
189
        }
190
191
        /*
192
         * We prefer to use stable orientations only, but allow unstable ones if
193
         * the item doesn't fit in the box any other way
194
         */
195 73
        if (count($stableOrientations) > 0) {
196 71
            $orientationsToUse = $stableOrientations;
197 66
        } elseif (count($unstableOrientations) > 0) {
198 7
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
199
200 7
            if (count($stableOrientationsInEmptyBox) === 0) {
201 6
                $orientationsToUse = $unstableOrientations;
202
            }
203
        }
204
205 73
        return $orientationsToUse;
206
    }
207
208
    /**
209
     * Return the orientations for this item if it were to be placed into the box with nothing else.
210
     */
211 7
    protected function getStableOrientationsInEmptyBox(Item $item): array
212
    {
213 7
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
214
215 7
        return array_filter(
216 7
            $orientationsInEmptyBox,
217 7
            function (OrientatedItem $orientation) {
218 7
                return $orientation->isStable();
219 7
            }
220
        );
221
    }
222
223 76
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
224
    {
225 76
        $permutations = [];
226
227
        //Special case items that are the same as what we just packed - keep orientation
228 76
        if ($prevItem && $prevItem->isSameDimensions($item)) {
229 59
            $permutations[] = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
230
        } else {
231 76
            $permutations[] = [$item->getWidth(), $item->getLength(), $item->getDepth()];
232
233 76
            if ($item->getAllowedRotations() > 1) { //simple 2D rotation
234 75
                $permutations[] = [$item->getLength(), $item->getWidth(), $item->getDepth()];
235
            }
236
237 76
            if ($item->getAllowedRotations() === Item::ROTATION_BEST_FIT) { //add 3D rotation if we're allowed
238 49
                $permutations[] = [$item->getWidth(), $item->getDepth(), $item->getLength()];
239 49
                $permutations[] = [$item->getLength(), $item->getDepth(), $item->getWidth()];
240 49
                $permutations[] = [$item->getDepth(), $item->getWidth(), $item->getLength()];
241 49
                $permutations[] = [$item->getDepth(), $item->getLength(), $item->getWidth()];
242
            }
243
        }
244
245 76
        return $permutations;
246
    }
247
}
248