Passed
Push — 2.x-dev ( b4081b...4a1f8c )
by Doug
02:50
created

OrientatedItemFactory::getBestOrientation()   C

Complexity

Conditions 17
Paths 2

Size

Total Lines 71
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 17.1541

Importance

Changes 7
Bugs 3 Features 0
Metric Value
cc 17
eloc 36
c 7
b 3
f 0
nc 2
nop 12
dl 0
loc 71
ccs 34
cts 37
cp 0.9189
crap 17.1541
rs 5.2166

How to fix   Long Method    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
namespace DVDoug\BoxPacker;
8
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerAwareTrait;
11
12
/**
13
 * Figure out orientations for an item and a given set of dimensions.
14
 *
15
 * @author Doug Wright
16
 * @internal
17
 */
18
class OrientatedItemFactory implements LoggerAwareInterface
19
{
20
    use LoggerAwareTrait;
21
22
    /** @var Box */
23
    protected $box;
24
25
    /**
26
     * @var OrientatedItem[]
27
     */
28
    protected static $emptyBoxCache = [];
29
30 20
    public function __construct(Box $box)
31
    {
32 20
        $this->box = $box;
33 20
    }
34
35
    /**
36
     * Get the best orientation for an item.
37
     *
38
     * @param Item                $item
39
     * @param OrientatedItem|null $prevItem
40
     * @param ItemList            $nextItems
41
     * @param bool                $isLastItem
42
     * @param int                 $widthLeft
43
     * @param int                 $lengthLeft
44
     * @param int                 $depthLeft
45
     * @param int                 $rowLength
46
     * @param int                 $x
47
     * @param int                 $y
48
     * @param int                 $z
49
     * @param PackedItemList      $prevPackedItemList
50
     *
51
     * @return OrientatedItem|null
52
     */
53 20
    public function getBestOrientation(
54
        Item $item,
55
        OrientatedItem $prevItem = null,
56
        ItemList $nextItems,
57
        $isLastItem,
58
        $widthLeft,
59
        $lengthLeft,
60
        $depthLeft,
61
        $rowLength,
62
        $x,
63
        $y,
64
        $z,
65
        PackedItemList $prevPackedItemList
66
    ) {
67 20
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
68 20
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations, $isLastItem);
69
70 20
        if (empty($usableOrientations)) {
71 19
            return null;
72
        }
73
74
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList) {
75 9
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
76 9
            $orientationALengthLeft = $lengthLeft - $a->getLength();
77 9
            $orientationADepthLeft = $depthLeft - $a->getDepth();
78 9
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
79 9
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
80 9
            $orientationBDepthLeft = $depthLeft - $b->getDepth();
81
82 9
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
83 9
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
84
85 9
            if ($orientationAMinGap === 0 && ($orientationBMinGap !== 0 || PHP_MAJOR_VERSION > 5)) { // prefer A if it leaves no gap
86 4
                return -1;
87
            }
88 6
            if ($orientationBMinGap === 0) { // prefer B if it leaves no gap
89 2
                return 1;
90
            }
91
92
            // prefer leaving room for next item in current row
93 5
            if ($nextItems->count()) {
94 4
                $nextItemFitA = count($this->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList));
95 4
                $nextItemFitB = count($this->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList));
96 4
                if ($nextItemFitA && !$nextItemFitB) {
97
                    return -1;
98
                }
99 4
                if ($nextItemFitB && !$nextItemFitA) {
100 2
                    return 1;
101
                }
102
103
                // if not an easy either/or, do a partial lookahead
104 2
                $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
105 2
                $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength);
106 2
                if ($additionalPackedA > $additionalPackedB) {
107
                    return -1;
108
                }
109 2
                if ($additionalPackedA < $additionalPackedB) {
110 1
                    return 1;
111
                }
112 1
                if ($additionalPackedA === 0) {
113
                    return PHP_MAJOR_VERSION > 5 ? -1 : 1;
114
                }
115
            }
116
            // otherwise prefer leaving minimum possible gap, or the greatest footprint
117 2
            return ($orientationADepthLeft - $orientationBDepthLeft) ?: ($orientationAMinGap - $orientationBMinGap) ?: ($a->getSurfaceFootprint() - $b->getSurfaceFootprint());
118 20
        });
119
120 20
        $bestFit = reset($usableOrientations);
121 20
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
122
123 20
        return $bestFit;
124
    }
125
126
    /**
127
     * Find all possible orientations for an item.
128
     *
129
     * @param Item                $item
130
     * @param OrientatedItem|null $prevItem
131
     * @param int                 $widthLeft
132
     * @param int                 $lengthLeft
133
     * @param int                 $depthLeft
134
     * @param int                 $x
135
     * @param int                 $y
136
     * @param int                 $z
137
     * @param PackedItemList      $prevPackedItemList
138
     *
139
     * @return OrientatedItem[]
140
     */
141 20
    public function getPossibleOrientations(
142
        Item $item,
143
        OrientatedItem $prevItem = null,
144
        $widthLeft,
145
        $lengthLeft,
146
        $depthLeft,
147
        $x,
148
        $y,
149
        $z,
150
        PackedItemList $prevPackedItemList
151
    ) {
152 20
        $orientations = [];
153
154
        //Special case items that are the same as what we just packed - keep orientation
155 20
        if ($prevItem && $this->isSameDimensions($prevItem->getItem(), $item)) {
156 15
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
157
        } else {
158
            //simple 2D rotation
159 20
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
160 20
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
161
162
            //add 3D rotation if we're allowed
163 20
            if (!$item->getKeepFlat()) {
164 12
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
165 12
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
166 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
167 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
168
            }
169
        }
170
171 20
        $orientations = array_unique($orientations);
172
173
        //remove any that simply don't fit
174
        $orientations = array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
175 20
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
176 20
        });
177
178 20
        if ($item instanceof ConstrainedPlacementItem) {
179
            $box = $this->box;
180
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) {
181
                /** @var ConstrainedPlacementItem $constrainedItem */
182
                $constrainedItem = $i->getItem();
183
184
                return $constrainedItem->canBePacked($box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
185
            });
186
        }
187
188 20
        return $orientations;
189
    }
190
191
    /**
192
     * @param  Item             $item
193
     * @return OrientatedItem[]
194
     */
195 20
    public function getPossibleOrientationsInEmptyBox(Item $item)
196
    {
197 20
        $cacheKey = $item->getWidth() .
198 20
            '|' .
199 20
            $item->getLength() .
200 20
            '|' .
201 20
            $item->getDepth() .
202 20
            '|' .
203 20
            ($item->getKeepFlat() ? '2D' : '3D') .
204 20
            '|' .
205 20
            $this->box->getInnerWidth() .
206 20
            '|' .
207 20
            $this->box->getInnerLength() .
208 20
            '|' .
209 20
            $this->box->getInnerDepth();
210
211 20
        if (isset(static::$emptyBoxCache[$cacheKey])) {
212 16
            $orientations = static::$emptyBoxCache[$cacheKey];
213
        } else {
214 19
            $orientations = $this->getPossibleOrientations(
215 19
                $item,
216 19
                null,
217 19
                $this->box->getInnerWidth(),
218 19
                $this->box->getInnerLength(),
219 19
                $this->box->getInnerDepth(),
220 19
                0,
221 19
                0,
222 19
                0,
223 19
                new PackedItemList()
224
            );
225 19
            static::$emptyBoxCache[$cacheKey] = $orientations;
226
        }
227
228 20
        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...
229
    }
230
231
    /**
232
     * @param Item             $item
233
     * @param OrientatedItem[] $possibleOrientations
234
     * @param bool             $isLastItem
235
     *
236
     * @return OrientatedItem[]
237
     */
238 20
    protected function getUsableOrientations(
239
        Item $item,
240
        $possibleOrientations,
241
        $isLastItem
242
    ) {
243 20
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
244
245
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
246 20
        foreach ($possibleOrientations as $orientation) {
247 20
            if ($orientation->isStable()) {
248 20
                $stableOrientations[] = $orientation;
249
            } else {
250 20
                $unstableOrientations[] = $orientation;
251
            }
252
        }
253
254
        /*
255
         * We prefer to use stable orientations only, but allow unstable ones if either
256
         * the item is the last one left to pack OR
257
         * the item doesn't fit in the box any other way
258
         */
259 20
        if (count($stableOrientations) > 0) {
260 20
            $orientationsToUse = $stableOrientations;
261 19
        } elseif (count($unstableOrientations) > 0) {
262
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
263
264
            if ($isLastItem || count($stableOrientationsInEmptyBox) === 0) {
265
                $orientationsToUse = $unstableOrientations;
266
            }
267
        }
268
269 20
        return $orientationsToUse;
270
    }
271
272
    /**
273
     * Return the orientations for this item if it were to be placed into the box with nothing else.
274
     *
275
     * @param  Item  $item
276
     * @return array
277
     */
278
    protected function getStableOrientationsInEmptyBox(Item $item)
279
    {
280
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
281
282
        return array_filter(
283
            $orientationsInEmptyBox,
284
            function (OrientatedItem $orientation) {
285
                return $orientation->isStable();
286
            }
287
        );
288
    }
289
290
    /**
291
     * Compare two items to see if they have same dimensions.
292
     *
293
     * @param Item $itemA
294
     * @param Item $itemB
295
     *
296
     * @return bool
297
     */
298 18
    protected function isSameDimensions(Item $itemA, Item $itemB)
299
    {
300 18
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
301 18
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
302 18
        sort($itemADimensions);
303 18
        sort($itemBDimensions);
304
305 18
        return $itemADimensions === $itemBDimensions;
306
    }
307
308
    /**
309
     * Approximation of a forward-looking packing.
310
     *
311
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
312
     * purely on fit.
313
     *
314
     * @param  OrientatedItem $prevItem
315
     * @param  ItemList       $nextItems
316
     * @param  int            $originalWidthLeft
317
     * @param  int            $originalLengthLeft
318
     * @param  int            $depthLeft
319
     * @param  int            $currentRowLengthBeforePacking
320
     * @return int
321
     */
322 2
    protected function calculateAdditionalItemsPackedWithThisOrientation(
323
        OrientatedItem $prevItem,
324
        ItemList $nextItems,
325
        $originalWidthLeft,
326
        $originalLengthLeft,
327
        $depthLeft,
328
        $currentRowLengthBeforePacking
329
    ) {
330 2
        $packedCount = 0;
331
332 2
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
333
334 2
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
335
336 2
        $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
337 2
        $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
338 2
        $tempPacker->setLookAheadMode(true);
339 2
        $remainingRowPacked = $tempPacker->pack();
340
        /** @var PackedItem $packedItem */
341 2
        foreach ($remainingRowPacked->getItems() as $packedItem) {
342 2
            $itemsToPack->remove($packedItem);
343
        }
344
345 2
        $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
346 2
        $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
347 2
        $tempPacker->setLookAheadMode(true);
348 2
        $nextRowsPacked = $tempPacker->pack();
349
        /** @var PackedItem $packedItem */
350 2
        foreach ($nextRowsPacked->getItems() as $packedItem) {
351 1
            $itemsToPack->remove($packedItem);
352
        }
353
354 2
        $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
355
356 2
        return $nextItems->count() - $itemsToPack->count();
357
    }
358
}
359