Passed
Push — 1.x-dev ( cc1b76...09423a )
by Doug
02:10
created

OrientatedItemFactory   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Test Coverage

Coverage 98.78%

Importance

Changes 0
Metric Value
dl 0
loc 203
ccs 81
cts 82
cp 0.9878
rs 10
c 0
b 0
f 0
wmc 25

6 Methods

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