Passed
Push — 1.x-dev ( 9106e4...cc1b76 )
by Doug
02:13
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 19
    public function getBestOrientation(
42
        Box $box,
43
        Item $item,
44
        $prevItem,
45
        $nextItem,
46
        $isLastItem,
47
        $widthLeft,
48
        $lengthLeft,
49
        $depthLeft
50
    ) {
51 19
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
52 19
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
53
54 19
        if (empty($usableOrientations)) {
55 19
            return;
56
        }
57
58 19
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
59 19
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
60 19
            $orientationALengthLeft = $lengthLeft - $a->getLength();
61 19
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
62 19
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
63
64 19
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
65 19
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
66
67 19
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
68 7
                return -1;
69 15
            } 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 14
                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 13
                return min($orientationAWidthLeft, $orientationALengthLeft) - min($orientationBWidthLeft, $orientationBLengthLeft);
83
            }
84 19
        });
85
86 19
        $bestFit = reset($usableOrientations);
87 19
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
88
89 19
        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 19
    public function getPossibleOrientations(
104
        Item $item,
105
        $prevItem,
106
        $widthLeft,
107
        $lengthLeft,
108
        $depthLeft
109
    ) {
110 19
        $orientations = [];
111
112
        //Special case items that are the same as what we just packed - keep orientation
113
        /* @noinspection PhpNonStrictObjectEqualityInspection */
114 19
        if ($prevItem && $prevItem->getItem() == $item) {
115 12
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
116
        } else {
117
            //simple 2D rotation
118 19
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
119 19
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
120
        }
121
122
        //remove any that simply don't fit
123 19
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
124 19
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
125 19
        });
126
    }
127
128
    /**
129
     * @param Item $item
130
     * @param Box  $box
131
     *
132
     * @return OrientatedItem[]
133
     */
134 19
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box)
135
    {
136 19
        $cacheKey = $item->getWidth().
137 19
            '|'.
138 19
            $item->getLength().
139 19
            '|'.
140 19
            $item->getDepth().
141 19
            '|'.
142 19
            $box->getInnerWidth().
143 19
            '|'.
144 19
            $box->getInnerLength().
145 19
            '|'.
146 19
            $box->getInnerDepth();
147
148 19
        if (isset(static::$emptyBoxCache[$cacheKey])) {
149 16
            $orientations = static::$emptyBoxCache[$cacheKey];
150
        } else {
151 17
            $orientations = $this->getPossibleOrientations(
152 17
                $item,
153 17
                null,
154 17
                $box->getInnerWidth(),
155 17
                $box->getInnerLength(),
156 17
                $box->getInnerDepth()
157
            );
158 17
            static::$emptyBoxCache[$cacheKey] = $orientations;
159
        }
160
161 19
        return $orientations;
162
    }
163
164
    /**
165
     * @param OrientatedItem[] $possibleOrientations
166
     * @param Box              $box
167
     * @param Item             $item
168
     * @param bool             $isLastItem
169
     *
170
     * @return OrientatedItem[]
171
     */
172 19
    protected function getUsableOrientations(
173
        $possibleOrientations,
174
        Box $box,
175
        Item $item,
176
        $isLastItem
177
    ) {
178
        /*
179
         * Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
180
         */
181 19
        $stableOrientations = [];
182 19
        $unstableOrientations = [];
183
184 19
        foreach ($possibleOrientations as $o => $orientation) {
185 19
            if ($orientation->isStable()) {
186 19
                $stableOrientations[] = $orientation;
187
            } else {
188 19
                $unstableOrientations[] = $orientation;
189
            }
190
        }
191
192 19
        $orientationsToUse = [];
193
194
        /*
195
         * We prefer to use stable orientations only, but allow unstable ones if either
196
         * the item is the last one left to pack OR
197
         * the item doesn't fit in the box any other way
198
         */
199 19
        if (count($stableOrientations) > 0) {
200 19
            $orientationsToUse = $stableOrientations;
201 19
        } elseif (count($unstableOrientations) > 0) {
202 1
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
203
204 1
            $stableOrientationsInEmptyBox = array_filter(
205 1
                $orientationsInEmptyBox,
206 1
                function (OrientatedItem $orientation) {
207 1
                    return $orientation->isStable();
208 1
                }
209
            );
210
211 1
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
212 1
                $orientationsToUse = $unstableOrientations;
213
            }
214
        }
215
216 19
        return $orientationsToUse;
217
    }
218
}
219