Passed
Push — weight_redistributor ( b7d3a0...d43d9c )
by Doug
02:10
created

OrientatedItemFactory   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 206
Duplicated Lines 0 %

Test Coverage

Coverage 98.81%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 206
ccs 83
cts 84
cp 0.9881
rs 10
c 1
b 0
f 0
wmc 25

4 Methods

Rating   Name   Duplication   Size   Complexity  
D getBestOrientation() 0 45 9
B getPossibleOrientations() 0 31 6
C getUsableOrientations() 0 45 7
B getPossibleOrientationsInEmptyBox() 0 30 3
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 20
    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 20
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
53 20
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
54
55 20
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
56 20
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
57 20
            $orientationALengthLeft = $lengthLeft - $a->getLength();
58 20
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
59 20
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
60
61 20
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
62 20
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
63
64 20
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
65 7
                return -1;
66 16
            } elseif ($orientationBMinGap === 0) { // prefer B if it leaves no gap
67 1
                return 1;
68
            } else { // prefer leaving room for next item in current row
69 15
                if ($nextItem) {
70 14
                    $nextItemFitA = count($this->getPossibleOrientations($nextItem, $a, $orientationAWidthLeft, $orientationALengthLeft, $depthLeft));
71 14
                    $nextItemFitB = count($this->getPossibleOrientations($nextItem, $b, $orientationBWidthLeft, $orientationBLengthLeft, $depthLeft));
72 14
                    if ($nextItemFitA && !$nextItemFitB) {
73
                        return -1;
74 14
                    } elseif ($nextItemFitB && !$nextItemFitA) {
75 1
                        return 1;
76
                    }
77
                }
78
                // otherwise prefer leaving minimum possible gap
79 14
                return min($orientationAWidthLeft, $orientationALengthLeft) <=> min($orientationBWidthLeft, $orientationBLengthLeft);
80
            }
81 20
        });
82
83 20
        $bestFit = reset($usableOrientations) ?: null;
84 20
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
85
86 20
        return $bestFit;
87
    }
88
89
    /**
90
     * Find all possible orientations for an item.
91
     *
92
     * @param Item                $item
93
     * @param OrientatedItem|null $prevItem
94
     * @param int                 $widthLeft
95
     * @param int                 $lengthLeft
96
     * @param int                 $depthLeft
97
     *
98
     * @return OrientatedItem[]
99
     */
100 20
    public function getPossibleOrientations(
101
        Item $item,
102
        ?OrientatedItem $prevItem,
103
        int $widthLeft,
104
        int $lengthLeft,
105
        int $depthLeft
106
    ): array {
107 20
        $orientations = [];
108
109
        //Special case items that are the same as what we just packed - keep orientation
110
        /* @noinspection PhpNonStrictObjectEqualityInspection */
111 20
        if ($prevItem && $prevItem->getItem() == $item) {
112 12
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
113
        } else {
114
115
            //simple 2D rotation
116 20
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
117 20
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
118
119
            //add 3D rotation if we're allowed
120 20
            if (!$item->getKeepFlat()) {
121 12
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
122 12
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
123 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
124 12
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
125
            }
126
        }
127
128
        //remove any that simply don't fit
129 20
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
130 20
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
131 20
        });
132
    }
133
134
    /**
135
     * @param Item $item
136
     * @param Box  $box
137
     *
138
     * @return OrientatedItem[]
139
     */
140 20
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box): array
141
    {
142 20
        $cacheKey = $item->getWidth().
143 20
            '|'.
144 20
            $item->getLength().
145 20
            '|'.
146 20
            $item->getDepth().
147 20
            '|'.
148 20
            ($item->getKeepFlat() ? '2D' : '3D').
149 20
            '|'.
150 20
            $box->getInnerWidth().
151 20
            '|'.
152 20
            $box->getInnerLength().
153 20
            '|'.
154 20
            $box->getInnerDepth();
155
156 20
        if (isset(static::$emptyBoxCache[$cacheKey])) {
157 17
            $orientations = static::$emptyBoxCache[$cacheKey];
158
        } else {
159 19
            $orientations = $this->getPossibleOrientations(
160 19
                $item,
161 19
                null,
162 19
                $box->getInnerWidth(),
163 19
                $box->getInnerLength(),
164 19
                $box->getInnerDepth()
165
            );
166 19
            static::$emptyBoxCache[$cacheKey] = $orientations;
167
        }
168
169 20
        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...
170
    }
171
172
    /**
173
     * @param OrientatedItem[] $possibleOrientations
174
     * @param Box              $box
175
     * @param Item             $item
176
     * @param bool             $isLastItem
177
     *
178
     * @return OrientatedItem[]
179
     */
180 20
    protected function getUsableOrientations(
181
        $possibleOrientations,
182
        Box $box,
183
        Item $item,
184
        bool $isLastItem
185
    ): array {
186
        /*
187
         * Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
188
         */
189 20
        $stableOrientations = [];
190 20
        $unstableOrientations = [];
191
192 20
        foreach ($possibleOrientations as $o => $orientation) {
193 20
            if ($orientation->isStable()) {
194 19
                $stableOrientations[] = $orientation;
195
            } else {
196 20
                $unstableOrientations[] = $orientation;
197
            }
198
        }
199
200 20
        $orientationsToUse = [];
201
202
        /*
203
         * We prefer to use stable orientations only, but allow unstable ones if either
204
         * the item is the last one left to pack OR
205
         * the item doesn't fit in the box any other way
206
         */
207 20
        if (count($stableOrientations) > 0) {
208 19
            $orientationsToUse = $stableOrientations;
209 20
        } elseif (count($unstableOrientations) > 0) {
210 2
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
211
212 2
            $stableOrientationsInEmptyBox = array_filter(
213 2
                $orientationsInEmptyBox,
214 2
                function (OrientatedItem $orientation) {
215 2
                    return $orientation->isStable();
216 2
                }
217
            );
218
219 2
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
220 2
                $orientationsToUse = $unstableOrientations;
221
            }
222
        }
223
224 20
        return $orientationsToUse;
225
    }
226
}
227