Passed
Push — 1.x-dev ( 9106e4...cc1b76 )
by Doug
02:13
created

OrientatedItemFactory   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 201
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.73%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 25
c 1
b 0
f 0
lcom 1
cbo 5
dl 0
loc 201
ccs 78
cts 79
cp 0.9873
rs 10

4 Methods

Rating   Name   Duplication   Size   Complexity  
C getBestOrientation() 0 50 11
B getPossibleOrientations() 0 24 5
B getPossibleOrientationsInEmptyBox() 0 29 2
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 19
    public function getBestOrientation(
42
        Box $box,
43
        Item $item,
44
        $prevItem,
45
        $nextItem,
46
        $isLastItem,
47
        $widthLeft,
48
        $lengthLeft,
49
        $depthLeft
50
    ) {
51 19
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
52 19
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
53
54 19
        if (empty($usableOrientations)) {
55 19
            return;
56
        }
57
58 19
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
59 19
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
60 19
            $orientationALengthLeft = $lengthLeft - $a->getLength();
61 19
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
62 19
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
63
64 19
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
65 19
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
66
67 19
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
68 7
                return -1;
69 15
            } 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 14
                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 13
                return min($orientationAWidthLeft, $orientationALengthLeft) - min($orientationBWidthLeft, $orientationBLengthLeft);
83
            }
84 19
        });
85
86 19
        $bestFit = reset($usableOrientations);
87 19
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
88
89 19
        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 19
    public function getPossibleOrientations(
104
        Item $item,
105
        $prevItem,
106
        $widthLeft,
107
        $lengthLeft,
108
        $depthLeft
109
    ) {
110 19
        $orientations = [];
111
112
        //Special case items that are the same as what we just packed - keep orientation
113
        /* @noinspection PhpNonStrictObjectEqualityInspection */
114 19
        if ($prevItem && $prevItem->getItem() == $item) {
115 12
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
116
        } else {
117
            //simple 2D rotation
118 19
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
119 19
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
120
        }
121
122
        //remove any that simply don't fit
123 19
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
124 19
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
125 19
        });
126
    }
127
128
    /**
129
     * @param Item $item
130
     * @param Box  $box
131
     *
132
     * @return OrientatedItem[]
133
     */
134 19
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box)
135
    {
136 19
        $cacheKey = $item->getWidth().
137 19
            '|'.
138 19
            $item->getLength().
139 19
            '|'.
140 19
            $item->getDepth().
141 19
            '|'.
142 19
            $box->getInnerWidth().
143 19
            '|'.
144 19
            $box->getInnerLength().
145 19
            '|'.
146 19
            $box->getInnerDepth();
147
148 19
        if (isset(static::$emptyBoxCache[$cacheKey])) {
149 16
            $orientations = static::$emptyBoxCache[$cacheKey];
150
        } else {
151 17
            $orientations = $this->getPossibleOrientations(
152 17
                $item,
153 17
                null,
154 17
                $box->getInnerWidth(),
155 17
                $box->getInnerLength(),
156 17
                $box->getInnerDepth()
157
            );
158 17
            static::$emptyBoxCache[$cacheKey] = $orientations;
159
        }
160
161 19
        return $orientations;
162
    }
163
164
    /**
165
     * @param OrientatedItem[] $possibleOrientations
166
     * @param Box              $box
167
     * @param Item             $item
168
     * @param bool             $isLastItem
169
     *
170
     * @return OrientatedItem[]
171
     */
172 19
    protected function getUsableOrientations(
173
        $possibleOrientations,
174
        Box $box,
175
        Item $item,
176
        $isLastItem
177
    ) {
178
        /*
179
         * Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
180
         */
181 19
        $stableOrientations = [];
182 19
        $unstableOrientations = [];
183
184 19
        foreach ($possibleOrientations as $o => $orientation) {
185 19
            if ($orientation->isStable()) {
186 19
                $stableOrientations[] = $orientation;
187
            } else {
188 19
                $unstableOrientations[] = $orientation;
189
            }
190
        }
191
192 19
        $orientationsToUse = [];
193
194
        /*
195
         * We prefer to use stable orientations only, but allow unstable ones if either
196
         * the item is the last one left to pack OR
197
         * the item doesn't fit in the box any other way
198
         */
199 19
        if (count($stableOrientations) > 0) {
200 19
            $orientationsToUse = $stableOrientations;
201 19
        } elseif (count($unstableOrientations) > 0) {
202 1
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
203
204 1
            $stableOrientationsInEmptyBox = array_filter(
205 1
                $orientationsInEmptyBox,
206 1
                function (OrientatedItem $orientation) {
207 1
                    return $orientation->isStable();
208 1
                }
209
            );
210
211 1
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
212 1
                $orientationsToUse = $unstableOrientations;
213
            }
214
        }
215
216 19
        return $orientationsToUse;
217
    }
218
}
219