Passed
Push — 1.x-dev ( 3133ae...12da21 )
by Doug
03:08 queued 01:21
created

OrientatedItemFactory::getBestOrientation()   C

Complexity

Conditions 12
Paths 2

Size

Total Lines 47
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 12.0585

Importance

Changes 0
Metric Value
cc 12
eloc 27
nc 2
nop 6
dl 0
loc 47
ccs 25
cts 27
cp 0.9259
crap 12.0585
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

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:

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