Passed
Push — behat ( a74de5...4872f1 )
by Doug
02:39
created

OrientatedItemFactory::getBestOrientation()   C

Complexity

Conditions 11
Paths 2

Size

Total Lines 50
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

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

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
    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
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
53
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
54
55
        if (empty($usableOrientations)) {
56
            return null;
57
        }
58
59
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
60
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
61
            $orientationALengthLeft = $lengthLeft - $a->getLength();
62
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
63
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
64
65
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
66
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
67
68
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
69
                return -1;
70
            } elseif ($orientationBMinGap === 0) { // prefer B if it leaves no gap
71
                return 1;
72
            } else { // prefer leaving room for next item in current row
73
                if ($nextItem) {
74
                    $nextItemFitA = count($this->getPossibleOrientations($nextItem, $a, $orientationAWidthLeft, $orientationALengthLeft, $depthLeft));
75
                    $nextItemFitB = count($this->getPossibleOrientations($nextItem, $b, $orientationBWidthLeft, $orientationBLengthLeft, $depthLeft));
76
                    if ($nextItem && $nextItemFitA && !$nextItemFitB) {
77
                        return -1;
78
                    } elseif ($nextItem && $nextItemFitB && !$nextItemFitA) {
79
                        return 1;
80
                    }
81
                }
82
                // otherwise prefer leaving minimum possible gap
83
                return min($orientationAWidthLeft, $orientationALengthLeft) <=> min($orientationBWidthLeft, $orientationBLengthLeft);
84
            }
85
        });
86
87
        $bestFit = reset($usableOrientations);
88
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
89
90
        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
    public function getPossibleOrientations(
105
        Item $item,
106
        ?OrientatedItem $prevItem,
107
        int $widthLeft,
108
        int $lengthLeft,
109
        int $depthLeft
110
    ): array {
111
        $orientations = [];
112
113
        //Special case items that are the same as what we just packed - keep orientation
114
        /* @noinspection PhpNonStrictObjectEqualityInspection */
115
        if ($prevItem && $prevItem->getItem() == $item) {
116
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
117
        } else {
118
119
            //simple 2D rotation
120
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
121
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
122
123
            //add 3D rotation if we're allowed
124
            if (!$item->getKeepFlat()) {
125
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
126
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
127
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
128
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
129
            }
130
        }
131
132
        //remove any that simply don't fit
133
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
134
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
135
        });
136
    }
137
138
    /**
139
     * @param Item $item
140
     * @param Box  $box
141
     *
142
     * @return OrientatedItem[]
143
     */
144
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box): array
145
    {
146
        $cacheKey = $item->getWidth().
147
            '|'.
148
            $item->getLength().
149
            '|'.
150
            $item->getDepth().
151
            '|'.
152
            ($item->getKeepFlat() ? '2D' : '3D').
153
            '|'.
154
            $box->getInnerWidth().
155
            '|'.
156
            $box->getInnerLength().
157
            '|'.
158
            $box->getInnerDepth();
159
160
        if (isset(static::$emptyBoxCache[$cacheKey])) {
161
            $orientations = static::$emptyBoxCache[$cacheKey];
162
        } else {
163
            $orientations = $this->getPossibleOrientations(
164
                $item,
165
                null,
166
                $box->getInnerWidth(),
167
                $box->getInnerLength(),
168
                $box->getInnerDepth()
169
            );
170
            static::$emptyBoxCache[$cacheKey] = $orientations;
171
        }
172
173
        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
    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
        $stableOrientations = [];
194
        $unstableOrientations = [];
195
196
        foreach ($possibleOrientations as $o => $orientation) {
197
            if ($orientation->isStable()) {
198
                $stableOrientations[] = $orientation;
199
            } else {
200
                $unstableOrientations[] = $orientation;
201
            }
202
        }
203
204
        $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
        if (count($stableOrientations) > 0) {
212
            $orientationsToUse = $stableOrientations;
213
        } elseif (count($unstableOrientations) > 0) {
214
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
215
216
            $stableOrientationsInEmptyBox = array_filter(
217
                $orientationsInEmptyBox,
218
                function (OrientatedItem $orientation) {
219
                    return $orientation->isStable();
220
                }
221
            );
222
223
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
224
                $orientationsToUse = $unstableOrientations;
225
            }
226
        }
227
228
        return $orientationsToUse;
229
    }
230
}
231