Passed
Push — master ( efb060...599fbf )
by Doug
03:20
created

OrientatedItemFactory::getBestOrientation()   C

Complexity

Conditions 11
Paths 2

Size

Total Lines 50
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 11.0055

Importance

Changes 0
Metric Value
dl 0
loc 50
ccs 27
cts 28
cp 0.9643
rs 5.4893
c 0
b 0
f 0
cc 11
eloc 37
nc 2
nop 8
crap 11.0055

How to fix   Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 ($nextItem && $nextItemFitA && !$nextItemFitB) {
77
                        return -1;
78 14
                    } elseif ($nextItem && $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;
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