Passed
Push — weight_redistributor ( d43d9c...a34054 )
by Doug
02:59
created

OrientatedItemFactory::getBestOrientation()   C

Complexity

Conditions 9
Paths 2

Size

Total Lines 49
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 9.0036

Importance

Changes 0
Metric Value
cc 9
eloc 27
nc 2
nop 8
dl 0
loc 49
ccs 27
cts 28
cp 0.9643
crap 9.0036
rs 5.7446
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
        if (empty($usableOrientations)) {
56 19
            return null;
57
        }
58
59 20
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
60 20
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
61 20
            $orientationALengthLeft = $lengthLeft - $a->getLength();
62 20
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
63 20
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
64
65 20
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
66 20
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
67
68 20
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
69 7
                return -1;
70 16
            } elseif ($orientationBMinGap === 0) { // prefer B if it leaves no gap
71 1
                return 1;
72
            } else { // prefer leaving room for next item in current row
73 15
                if ($nextItem) {
74 14
                    $nextItemFitA = count($this->getPossibleOrientations($nextItem, $a, $orientationAWidthLeft, $orientationALengthLeft, $depthLeft));
75 14
                    $nextItemFitB = count($this->getPossibleOrientations($nextItem, $b, $orientationBWidthLeft, $orientationBLengthLeft, $depthLeft));
76 14
                    if ($nextItemFitA && !$nextItemFitB) {
77
                        return -1;
78 14
                    } elseif ($nextItemFitB && !$nextItemFitA) {
79 1
                        return 1;
80
                    }
81
                }
82
                // otherwise prefer leaving minimum possible gap
83 14
                return min($orientationAWidthLeft, $orientationALengthLeft) <=> min($orientationBWidthLeft, $orientationBLengthLeft);
84
            }
85 20
        });
86
87 20
        $bestFit = reset($usableOrientations);
88 20
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
89
90 20
        return $bestFit;
91
    }
92
93
    /**
94
     * Find all possible orientations for an item.
95
     *
96
     * @param Item                $item
97
     * @param OrientatedItem|null $prevItem
98
     * @param int                 $widthLeft
99
     * @param int                 $lengthLeft
100
     * @param int                 $depthLeft
101
     *
102
     * @return OrientatedItem[]
103
     */
104 20
    public function getPossibleOrientations(
105
        Item $item,
106
        ?OrientatedItem $prevItem,
107
        int $widthLeft,
108
        int $lengthLeft,
109
        int $depthLeft
110
    ): array {
111 20
        $orientations = [];
112
113
        //Special case items that are the same as what we just packed - keep orientation
114
        /* @noinspection PhpNonStrictObjectEqualityInspection */
115 20
        if ($prevItem && $prevItem->getItem() == $item) {
116 12
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
117
        } else {
118
119
            //simple 2D rotation
120 20
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
121 20
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
122
123
            //add 3D rotation if we're allowed
124 20
            if (!$item->getKeepFlat()) {
125 12
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
126 12
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
127 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
128 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
129
            }
130
        }
131
132
        //remove any that simply don't fit
133 20
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
134 20
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
135 20
        });
136
    }
137
138
    /**
139
     * @param Item $item
140
     * @param Box  $box
141
     *
142
     * @return OrientatedItem[]
143
     */
144 20
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box): array
145
    {
146 20
        $cacheKey = $item->getWidth().
147 20
            '|'.
148 20
            $item->getLength().
149 20
            '|'.
150 20
            $item->getDepth().
151 20
            '|'.
152 20
            ($item->getKeepFlat() ? '2D' : '3D').
153 20
            '|'.
154 20
            $box->getInnerWidth().
155 20
            '|'.
156 20
            $box->getInnerLength().
157 20
            '|'.
158 20
            $box->getInnerDepth();
159
160 20
        if (isset(static::$emptyBoxCache[$cacheKey])) {
161 17
            $orientations = static::$emptyBoxCache[$cacheKey];
162
        } else {
163 19
            $orientations = $this->getPossibleOrientations(
164 19
                $item,
165 19
                null,
166 19
                $box->getInnerWidth(),
167 19
                $box->getInnerLength(),
168 19
                $box->getInnerDepth()
169
            );
170 19
            static::$emptyBoxCache[$cacheKey] = $orientations;
171
        }
172
173 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...
174
    }
175
176
    /**
177
     * @param OrientatedItem[] $possibleOrientations
178
     * @param Box              $box
179
     * @param Item             $item
180
     * @param bool             $isLastItem
181
     *
182
     * @return OrientatedItem[]
183
     */
184 20
    protected function getUsableOrientations(
185
        $possibleOrientations,
186
        Box $box,
187
        Item $item,
188
        bool $isLastItem
189
    ): array {
190
        /*
191
         * Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
192
         */
193 20
        $stableOrientations = [];
194 20
        $unstableOrientations = [];
195
196 20
        foreach ($possibleOrientations as $o => $orientation) {
197 20
            if ($orientation->isStable()) {
198 19
                $stableOrientations[] = $orientation;
199
            } else {
200 20
                $unstableOrientations[] = $orientation;
201
            }
202
        }
203
204 20
        $orientationsToUse = [];
205
206
        /*
207
         * We prefer to use stable orientations only, but allow unstable ones if either
208
         * the item is the last one left to pack OR
209
         * the item doesn't fit in the box any other way
210
         */
211 20
        if (count($stableOrientations) > 0) {
212 19
            $orientationsToUse = $stableOrientations;
213 20
        } elseif (count($unstableOrientations) > 0) {
214 2
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
215
216 2
            $stableOrientationsInEmptyBox = array_filter(
217 2
                $orientationsInEmptyBox,
218 2
                function (OrientatedItem $orientation) {
219 2
                    return $orientation->isStable();
220 2
                }
221
            );
222
223 2
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
224 2
                $orientationsToUse = $unstableOrientations;
225
            }
226
        }
227
228 20
        return $orientationsToUse;
229
    }
230
}
231