Passed
Push — weight_redistributor ( b7d3a0...d43d9c )
by Doug
02:10
created

OrientatedItemFactory::getBestOrientation()   D

Complexity

Conditions 9
Paths 1

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 9.0046

Importance

Changes 0
Metric Value
cc 9
eloc 25
nc 1
nop 8
dl 0
loc 45
ccs 25
cts 26
cp 0.9615
crap 9.0046
rs 4.909
c 0
b 0
f 0

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\LoggerAwareTrait;
13
14
/**
15
 * Figure out orientations for an item and a given set of dimensions.
16
 *
17
 * @author Doug Wright
18
 */
19
class OrientatedItemFactory implements LoggerAwareInterface
20
{
21
    use LoggerAwareTrait;
22
23
    /**
24
     * @var OrientatedItem[]
25
     */
26
    protected static $emptyBoxCache = [];
27
28
    /**
29
     * Get the best orientation for an item.
30
     *
31
     * @param Box                 $box
32
     * @param Item                $item
33
     * @param OrientatedItem|null $prevItem
34
     * @param Item|null           $nextItem
35
     * @param bool                $isLastItem
36
     * @param int                 $widthLeft
37
     * @param int                 $lengthLeft
38
     * @param int                 $depthLeft
39
     *
40
     * @return OrientatedItem|null
41
     */
42 20
    public function getBestOrientation(
43
        Box $box,
44
        Item $item,
45
        ?OrientatedItem $prevItem,
46
        ?Item $nextItem,
47
        bool $isLastItem,
48
        int $widthLeft,
49
        int $lengthLeft,
50
        int $depthLeft
51
    ): ?OrientatedItem {
52 20
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
53 20
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
54
55 20
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
56 20
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
57 20
            $orientationALengthLeft = $lengthLeft - $a->getLength();
58 20
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
59 20
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
60
61 20
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
62 20
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
63
64 20
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
65 7
                return -1;
66 16
            } elseif ($orientationBMinGap === 0) { // prefer B if it leaves no gap
67 1
                return 1;
68
            } else { // prefer leaving room for next item in current row
69 15
                if ($nextItem) {
70 14
                    $nextItemFitA = count($this->getPossibleOrientations($nextItem, $a, $orientationAWidthLeft, $orientationALengthLeft, $depthLeft));
71 14
                    $nextItemFitB = count($this->getPossibleOrientations($nextItem, $b, $orientationBWidthLeft, $orientationBLengthLeft, $depthLeft));
72 14
                    if ($nextItemFitA && !$nextItemFitB) {
73
                        return -1;
74 14
                    } elseif ($nextItemFitB && !$nextItemFitA) {
75 1
                        return 1;
76
                    }
77
                }
78
                // otherwise prefer leaving minimum possible gap
79 14
                return min($orientationAWidthLeft, $orientationALengthLeft) <=> min($orientationBWidthLeft, $orientationBLengthLeft);
80
            }
81 20
        });
82
83 20
        $bestFit = reset($usableOrientations) ?: null;
84 20
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
85
86 20
        return $bestFit;
87
    }
88
89
    /**
90
     * Find all possible orientations for an item.
91
     *
92
     * @param Item                $item
93
     * @param OrientatedItem|null $prevItem
94
     * @param int                 $widthLeft
95
     * @param int                 $lengthLeft
96
     * @param int                 $depthLeft
97
     *
98
     * @return OrientatedItem[]
99
     */
100 20
    public function getPossibleOrientations(
101
        Item $item,
102
        ?OrientatedItem $prevItem,
103
        int $widthLeft,
104
        int $lengthLeft,
105
        int $depthLeft
106
    ): array {
107 20
        $orientations = [];
108
109
        //Special case items that are the same as what we just packed - keep orientation
110
        /* @noinspection PhpNonStrictObjectEqualityInspection */
111 20
        if ($prevItem && $prevItem->getItem() == $item) {
112 12
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
113
        } else {
114
115
            //simple 2D rotation
116 20
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
117 20
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
118
119
            //add 3D rotation if we're allowed
120 20
            if (!$item->getKeepFlat()) {
121 12
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
122 12
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
123 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
124 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
125
            }
126
        }
127
128
        //remove any that simply don't fit
129 20
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
130 20
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
131 20
        });
132
    }
133
134
    /**
135
     * @param Item $item
136
     * @param Box  $box
137
     *
138
     * @return OrientatedItem[]
139
     */
140 20
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box): array
141
    {
142 20
        $cacheKey = $item->getWidth().
143 20
            '|'.
144 20
            $item->getLength().
145 20
            '|'.
146 20
            $item->getDepth().
147 20
            '|'.
148 20
            ($item->getKeepFlat() ? '2D' : '3D').
149 20
            '|'.
150 20
            $box->getInnerWidth().
151 20
            '|'.
152 20
            $box->getInnerLength().
153 20
            '|'.
154 20
            $box->getInnerDepth();
155
156 20
        if (isset(static::$emptyBoxCache[$cacheKey])) {
157 17
            $orientations = static::$emptyBoxCache[$cacheKey];
158
        } else {
159 19
            $orientations = $this->getPossibleOrientations(
160 19
                $item,
161 19
                null,
162 19
                $box->getInnerWidth(),
163 19
                $box->getInnerLength(),
164 19
                $box->getInnerDepth()
165
            );
166 19
            static::$emptyBoxCache[$cacheKey] = $orientations;
167
        }
168
169 20
        return $orientations;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $orientations could return the type DVDoug\BoxPacker\OrientatedItem which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
170
    }
171
172
    /**
173
     * @param OrientatedItem[] $possibleOrientations
174
     * @param Box              $box
175
     * @param Item             $item
176
     * @param bool             $isLastItem
177
     *
178
     * @return OrientatedItem[]
179
     */
180 20
    protected function getUsableOrientations(
181
        $possibleOrientations,
182
        Box $box,
183
        Item $item,
184
        bool $isLastItem
185
    ): array {
186
        /*
187
         * Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
188
         */
189 20
        $stableOrientations = [];
190 20
        $unstableOrientations = [];
191
192 20
        foreach ($possibleOrientations as $o => $orientation) {
193 20
            if ($orientation->isStable()) {
194 19
                $stableOrientations[] = $orientation;
195
            } else {
196 20
                $unstableOrientations[] = $orientation;
197
            }
198
        }
199
200 20
        $orientationsToUse = [];
201
202
        /*
203
         * We prefer to use stable orientations only, but allow unstable ones if either
204
         * the item is the last one left to pack OR
205
         * the item doesn't fit in the box any other way
206
         */
207 20
        if (count($stableOrientations) > 0) {
208 19
            $orientationsToUse = $stableOrientations;
209 20
        } elseif (count($unstableOrientations) > 0) {
210 2
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
211
212 2
            $stableOrientationsInEmptyBox = array_filter(
213 2
                $orientationsInEmptyBox,
214 2
                function (OrientatedItem $orientation) {
215 2
                    return $orientation->isStable();
216 2
                }
217
            );
218
219 2
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
220 2
                $orientationsToUse = $unstableOrientations;
221
            }
222
        }
223
224 20
        return $orientationsToUse;
225
    }
226
}
227