Completed
Push — 2.x-dev ( 5f5b4d...48a1b4 )
by Doug
61:10 queued 51:42
created

OrientatedItemFactory   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 212
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.84%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 27
lcom 1
cbo 5
dl 0
loc 212
ccs 85
cts 86
cp 0.9884
rs 10
c 1
b 0
f 0

4 Methods

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