Passed
Push — 1.x-dev ( 8f914b...081a6e )
by Doug
04:31
created

OrientatedItemFactory::getPossibleOrientations()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 40
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.6829

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 6
eloc 15
c 1
b 1
f 0
nc 4
nop 9
dl 0
loc 40
ccs 11
cts 15
cp 0.7332
crap 6.6829
rs 9.2222

How to fix   Many Parameters   

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 19
    public function __construct(Box $box)
31
    {
32 19
        $this->box = $box;
33 19
    }
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 19
    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 19
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
68 19
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations, $isLastItem);
69
70 19
        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 7
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
76 7
            $orientationALengthLeft = $lengthLeft - $a->getLength();
77 7
            $orientationADepthLeft = $depthLeft - $a->getDepth();
78 7
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
79 7
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
80 7
            $orientationBDepthLeft = $depthLeft - $b->getDepth();
81
82 7
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
83 7
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
84
85 7
            if ($orientationAMinGap === 0 && ($orientationBMinGap !== 0 || PHP_MAJOR_VERSION > 5)) { // prefer A if it leaves no gap
86 2
                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 19
        });
119
120 19
        $bestFit = reset($usableOrientations);
121 19
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
122
123 19
        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 19
    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 19
        $orientations = [];
153
154
        //Special case items that are the same as what we just packed - keep orientation
155 19
        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 19
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
160 19
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
161
        }
162
163 19
        $orientations = array_unique($orientations);
164
165
        //remove any that simply don't fit
166
        $orientations = array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
167 19
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
168 19
        });
169
170 19
        if ($item instanceof ConstrainedPlacementItem) {
171
            $box = $this->box;
172
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) {
173
                /** @var ConstrainedPlacementItem $constrainedItem */
174
                $constrainedItem = $i->getItem();
175
176
                return $constrainedItem->canBePacked($box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
177
            });
178
        }
179
180 19
        return $orientations;
181
    }
182
183
    /**
184
     * @param  Item             $item
185
     * @return OrientatedItem[]
186
     */
187 19
    public function getPossibleOrientationsInEmptyBox(Item $item)
188
    {
189 19
        $cacheKey = $item->getWidth() .
190 19
            '|' .
191 19
            $item->getLength() .
192 19
            '|' .
193 19
            $item->getDepth() .
194 19
            '|' .
195 19
            $this->box->getInnerWidth() .
196 19
            '|' .
197 19
            $this->box->getInnerLength() .
198 19
            '|' .
199 19
            $this->box->getInnerDepth();
200
201 19
        if (isset(static::$emptyBoxCache[$cacheKey])) {
202 16
            $orientations = static::$emptyBoxCache[$cacheKey];
203
        } else {
204 17
            $orientations = $this->getPossibleOrientations(
205 17
                $item,
206 17
                null,
207 17
                $this->box->getInnerWidth(),
208 17
                $this->box->getInnerLength(),
209 17
                $this->box->getInnerDepth(),
210 17
                0,
211 17
                0,
212 17
                0,
213 17
                new PackedItemList()
214
            );
215 17
            static::$emptyBoxCache[$cacheKey] = $orientations;
216
        }
217
218 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...
219
    }
220
221
    /**
222
     * @param Item             $item
223
     * @param OrientatedItem[] $possibleOrientations
224
     * @param bool             $isLastItem
225
     *
226
     * @return OrientatedItem[]
227
     */
228 19
    protected function getUsableOrientations(
229
        Item $item,
230
        $possibleOrientations,
231
        $isLastItem
232
    ) {
233 19
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
234
235
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
236 19
        foreach ($possibleOrientations as $orientation) {
237 19
            if ($orientation->isStable()) {
238 19
                $stableOrientations[] = $orientation;
239
            } else {
240 19
                $unstableOrientations[] = $orientation;
241
            }
242
        }
243
244
        /*
245
         * We prefer to use stable orientations only, but allow unstable ones if either
246
         * the item is the last one left to pack OR
247
         * the item doesn't fit in the box any other way
248
         */
249 19
        if (count($stableOrientations) > 0) {
250 19
            $orientationsToUse = $stableOrientations;
251 19
        } elseif (count($unstableOrientations) > 0) {
252
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
253
254
            if ($isLastItem || count($stableOrientationsInEmptyBox) === 0) {
255
                $orientationsToUse = $unstableOrientations;
256
            }
257
        }
258
259 19
        return $orientationsToUse;
260
    }
261
262
    /**
263
     * Return the orientations for this item if it were to be placed into the box with nothing else.
264
     *
265
     * @param  Item  $item
266
     * @return array
267
     */
268
    protected function getStableOrientationsInEmptyBox(Item $item)
269
    {
270
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
271
272
        return array_filter(
273
            $orientationsInEmptyBox,
274
            function (OrientatedItem $orientation) {
275
                return $orientation->isStable();
276
            }
277
        );
278
    }
279
280
    /**
281
     * Compare two items to see if they have same dimensions.
282
     *
283
     * @param Item $itemA
284
     * @param Item $itemB
285
     *
286
     * @return bool
287
     */
288 18
    protected function isSameDimensions(Item $itemA, Item $itemB)
289
    {
290 18
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
291 18
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
292 18
        sort($itemADimensions);
293 18
        sort($itemBDimensions);
294
295 18
        return $itemADimensions === $itemBDimensions;
296
    }
297
298
    /**
299
     * Approximation of a forward-looking packing.
300
     *
301
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
302
     * purely on fit.
303
     *
304
     * @param  OrientatedItem $prevItem
305
     * @param  ItemList       $nextItems
306
     * @param  int            $originalWidthLeft
307
     * @param  int            $originalLengthLeft
308
     * @param  int            $depthLeft
309
     * @param  int            $currentRowLengthBeforePacking
310
     * @return int
311
     */
312 2
    protected function calculateAdditionalItemsPackedWithThisOrientation(
313
        OrientatedItem $prevItem,
314
        ItemList $nextItems,
315
        $originalWidthLeft,
316
        $originalLengthLeft,
317
        $depthLeft,
318
        $currentRowLengthBeforePacking
319
    ) {
320 2
        $packedCount = 0;
321
322 2
        $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking);
323
324 2
        $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow
325
326 2
        $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX);
327 2
        $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
328 2
        $tempPacker->setLookAheadMode(true);
329 2
        $remainingRowPacked = $tempPacker->pack();
330
        /** @var PackedItem $packedItem */
331 2
        foreach ($remainingRowPacked->getItems() as $packedItem) {
332 2
            $itemsToPack->remove($packedItem);
333
        }
334
335 2
        $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX);
336 2
        $tempPacker = new VolumePacker($tempBox, clone $itemsToPack);
337 2
        $tempPacker->setLookAheadMode(true);
338 2
        $nextRowsPacked = $tempPacker->pack();
339
        /** @var PackedItem $packedItem */
340 2
        foreach ($nextRowsPacked->getItems() as $packedItem) {
341 1
            $itemsToPack->remove($packedItem);
342
        }
343
344 2
        $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
345
346 2
        return $nextItems->count() - $itemsToPack->count();
347
    }
348
}
349