Passed
Push — master ( 946e2e...244ac2 )
by Doug
03:15
created

OrientatedItemFactory::getUsableOrientations()   C

Complexity

Conditions 7
Paths 12

Size

Total Lines 31
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 12
nop 2
dl 0
loc 31
ccs 13
cts 13
cp 1
crap 7
rs 6.7272
c 0
b 0
f 0
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
use function array_filter;
14
use function count;
15
use function min;
16
use function reset;
17
use function usort;
18
19
/**
20
 * Figure out orientations for an item and a given set of dimensions.
21
 *
22
 * @author Doug Wright
23
 */
24
class OrientatedItemFactory implements LoggerAwareInterface
25
{
26
    use LoggerAwareTrait;
27
28
    /** @var Item */
29
    protected $item;
30
31
    /** @var Box */
32
    protected $box;
33
34
    /**
35
     * @var OrientatedItem[]
36
     */
37
    protected static $emptyBoxCache = [];
38
39 22
    public function __construct(Item $item, Box $box)
40
    {
41 22
        $this->item = $item;
42 22
        $this->box = $box;
43 22
    }
44
45
    /**
46
     * Get the best orientation for an item.
47
     *
48
     * @param OrientatedItem|null $prevItem
49
     * @param Item|null           $nextItem
50
     * @param bool                $isLastItem
51
     * @param int                 $widthLeft
52
     * @param int                 $lengthLeft
53
     * @param int                 $depthLeft
54
     *
55
     * @return OrientatedItem|null
56
     */
57 21
    public function getBestOrientation(
58
        ?OrientatedItem $prevItem,
59
        ?Item $nextItem,
60
        bool $isLastItem,
61
        int $widthLeft,
62
        int $lengthLeft,
63
        int $depthLeft
64
    ): ?OrientatedItem {
65 21
        $possibleOrientations = $this->getPossibleOrientations($this->item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
66 21
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $isLastItem);
67
68 21
        if (empty($usableOrientations)) {
69 20
            return null;
70
        }
71
72
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
73 21
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
74 21
            $orientationALengthLeft = $lengthLeft - $a->getLength();
75 21
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
76 21
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
77
78 21
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
79 21
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
80
81 21
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
82 7
                return -1;
83 17
            } elseif ($orientationBMinGap === 0) { // prefer B if it leaves no gap
84 1
                return 1;
85
            } else { // prefer leaving room for next item in current row
86 16
                if ($nextItem) {
87 15
                    $nextItemFitA = count($this->getPossibleOrientations($nextItem, $a, $orientationAWidthLeft, $orientationALengthLeft, $depthLeft));
88 15
                    $nextItemFitB = count($this->getPossibleOrientations($nextItem, $b, $orientationBWidthLeft, $orientationBLengthLeft, $depthLeft));
89 15
                    if ($nextItemFitA && !$nextItemFitB) {
90
                        return -1;
91 15
                    } elseif ($nextItemFitB && !$nextItemFitA) {
92 1
                        return 1;
93
                    }
94
                }
95
                // otherwise prefer leaving minimum possible gap
96 15
                return min($orientationAWidthLeft, $orientationALengthLeft) <=> min($orientationBWidthLeft, $orientationBLengthLeft);
97
            }
98 21
        });
99
100 21
        $bestFit = reset($usableOrientations);
101 21
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
102
103 21
        return $bestFit;
104
    }
105
106
    /**
107
     * Find all possible orientations for an item.
108
     *
109
     * @param Item                $item
110
     * @param OrientatedItem|null $prevItem
111
     * @param int                 $widthLeft
112
     * @param int                 $lengthLeft
113
     * @param int                 $depthLeft
114
     *
115
     * @return OrientatedItem[]
116
     */
117 21
    public function getPossibleOrientations(
118
        Item $item,
119
        ?OrientatedItem $prevItem,
120
        int $widthLeft,
121
        int $lengthLeft,
122
        int $depthLeft
123
    ): array {
124 21
        $orientations = [];
125
126
        //Special case items that are the same as what we just packed - keep orientation
127
        /* @noinspection PhpNonStrictObjectEqualityInspection */
128 21
        if ($prevItem && $prevItem->getItem() == $item) {
129 13
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
130
        } else {
131
132
            //simple 2D rotation
133 21
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
134 21
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
135
136
            //add 3D rotation if we're allowed
137 21
            if (!$item->getKeepFlat()) {
138 13
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
139 13
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
140 13
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
141 13
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
142
            }
143
        }
144
145
        //remove any that simply don't fit
146
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
147 21
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
148 21
        });
149
    }
150
151
    /**
152
     * @return OrientatedItem[]
153
     */
154 22
    public function getPossibleOrientationsInEmptyBox(): array
155
    {
156 22
        $cacheKey = $this->item->getWidth().
157 22
            '|'.
158 22
            $this->item->getLength().
159 22
            '|'.
160 22
            $this->item->getDepth().
161 22
            '|'.
162 22
            ($this->item->getKeepFlat() ? '2D' : '3D').
163 22
            '|'.
164 22
            $this->box->getInnerWidth().
165 22
            '|'.
166 22
            $this->box->getInnerLength().
167 22
            '|'.
168 22
            $this->box->getInnerDepth();
169
170 22
        if (isset(static::$emptyBoxCache[$cacheKey])) {
171 22
            $orientations = static::$emptyBoxCache[$cacheKey];
172
        } else {
173 20
            $orientations = $this->getPossibleOrientations(
174 20
                $this->item,
175 20
                null,
176 20
                $this->box->getInnerWidth(),
177 20
                $this->box->getInnerLength(),
178 20
                $this->box->getInnerDepth()
179
            );
180 20
            static::$emptyBoxCache[$cacheKey] = $orientations;
181
        }
182
183 22
        return $orientations;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $orientations could return the type DVDoug\BoxPacker\OrientatedItem which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
184
    }
185
186
    /**
187
     * @param OrientatedItem[] $possibleOrientations
188
     * @param bool             $isLastItem
189
     *
190
     * @return OrientatedItem[]
191
     */
192 21
    protected function getUsableOrientations(
193
        $possibleOrientations,
194
        bool $isLastItem
195
    ): array {
196 21
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
197
198
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
199 21
        foreach ($possibleOrientations as $orientation) {
200 21
            if ($orientation->isStable()) {
201 20
                $stableOrientations[] = $orientation;
202
            } else {
203 21
                $unstableOrientations[] = $orientation;
204
            }
205
        }
206
207
        /*
208
         * We prefer to use stable orientations only, but allow unstable ones if either
209
         * the item is the last one left to pack OR
210
         * the item doesn't fit in the box any other way
211
         */
212 21
        if (count($stableOrientations) > 0) {
213 20
            $orientationsToUse = $stableOrientations;
214 21
        } elseif (count($unstableOrientations) > 0) {
215 2
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox();
216
217 2
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
218 2
                $orientationsToUse = $unstableOrientations;
219
            }
220
        }
221
222 21
        return $orientationsToUse;
223
    }
224
225
    /**
226
     * Return the orientations for this item if it were to be placed into the box with nothing else.
227
     *
228
     * @return array
229
     */
230 2
    protected function getStableOrientationsInEmptyBox(): array
231
    {
232 2
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox();
233
234 2
        return array_filter(
235 2
            $orientationsInEmptyBox,
236
            function (OrientatedItem $orientation) {
237 2
                return $orientation->isStable();
238 2
            }
239
        );
240
    }
241
}
242