Completed
Push — 2.x-dev ( 5f5b4d...48a1b4 )
by Doug
61:10 queued 51:42
created

OrientatedItemFactory::getBestOrientation()   C

Complexity

Conditions 11
Paths 2

Size

Total Lines 50
Code Lines 36

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