Passed
Push — weight_redistributor ( a34054...af2cc6 )
by Doug
01:56
created

getStableOrientationsInEmptyBox()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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