Passed
Push — weight_redistributor ( d43d9c...a34054 )
by Doug
02:59
created

OrientatedItemFactory   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Test Coverage

Coverage 98.84%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 210
ccs 85
cts 86
cp 0.9884
rs 10
c 1
b 0
f 0
wmc 25

4 Methods

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