Passed
Push — 2.x-dev ( e9f8a3...78c6d6 )
by Doug
04:07
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 48
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.392

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 7
eloc 20
c 1
b 1
f 0
nc 6
nop 9
dl 0
loc 48
ccs 16
cts 20
cp 0.8
crap 7.392
rs 8.6666

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