Passed
Push — behat ( a74de5...4872f1 )
by Doug
02:39
created

OrientatedItemFactory::getUsableOrientations()   C

Complexity

Conditions 7
Paths 12

Size

Total Lines 46
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 46
ccs 0
cts 20
cp 0
rs 6.7272
c 1
b 0
f 0
cc 7
eloc 25
nc 12
nop 4
crap 56
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
    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
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
53
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
54
55
        if (empty($usableOrientations)) {
56
            return null;
57
        }
58
59
        usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItem) {
60
            $orientationAWidthLeft = $widthLeft - $a->getWidth();
61
            $orientationALengthLeft = $lengthLeft - $a->getLength();
62
            $orientationBWidthLeft = $widthLeft - $b->getWidth();
63
            $orientationBLengthLeft = $lengthLeft - $b->getLength();
64
65
            $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
66
            $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
67
68
            if ($orientationAMinGap === 0) { // prefer A if it leaves no gap
69
                return -1;
70
            } elseif ($orientationBMinGap === 0) { // prefer B if it leaves no gap
71
                return 1;
72
            } else { // prefer leaving room for next item in current row
73
                if ($nextItem) {
74
                    $nextItemFitA = count($this->getPossibleOrientations($nextItem, $a, $orientationAWidthLeft, $orientationALengthLeft, $depthLeft));
75
                    $nextItemFitB = count($this->getPossibleOrientations($nextItem, $b, $orientationBWidthLeft, $orientationBLengthLeft, $depthLeft));
76
                    if ($nextItem && $nextItemFitA && !$nextItemFitB) {
77
                        return -1;
78
                    } elseif ($nextItem && $nextItemFitB && !$nextItemFitA) {
79
                        return 1;
80
                    }
81
                }
82
                // otherwise prefer leaving minimum possible gap
83
                return min($orientationAWidthLeft, $orientationALengthLeft) <=> min($orientationBWidthLeft, $orientationBLengthLeft);
84
            }
85
        });
86
87
        $bestFit = reset($usableOrientations);
88
        $this->logger->debug('Selected best fit orientation', ['orientation' => $bestFit]);
89
90
        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
    public function getPossibleOrientations(
105
        Item $item,
106
        ?OrientatedItem $prevItem,
107
        int $widthLeft,
108
        int $lengthLeft,
109
        int $depthLeft
110
    ): array {
111
        $orientations = [];
112
113
        //Special case items that are the same as what we just packed - keep orientation
114
        /* @noinspection PhpNonStrictObjectEqualityInspection */
115
        if ($prevItem && $prevItem->getItem() == $item) {
116
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
117
        } else {
118
119
            //simple 2D rotation
120
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
121
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
122
123
            //add 3D rotation if we're allowed
124
            if (!$item->getKeepFlat()) {
125
                $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
126
                $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
127
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
128
                $orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
129
            }
130
        }
131
132
        //remove any that simply don't fit
133
        return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
134
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
135
        });
136
    }
137
138
    /**
139
     * @param Item $item
140
     * @param Box  $box
141
     *
142
     * @return OrientatedItem[]
143
     */
144
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box): array
145
    {
146
        $cacheKey = $item->getWidth().
147
            '|'.
148
            $item->getLength().
149
            '|'.
150
            $item->getDepth().
151
            '|'.
152
            ($item->getKeepFlat() ? '2D' : '3D').
153
            '|'.
154
            $box->getInnerWidth().
155
            '|'.
156
            $box->getInnerLength().
157
            '|'.
158
            $box->getInnerDepth();
159
160
        if (isset(static::$emptyBoxCache[$cacheKey])) {
161
            $orientations = static::$emptyBoxCache[$cacheKey];
162
        } else {
163
            $orientations = $this->getPossibleOrientations(
164
                $item,
165
                null,
166
                $box->getInnerWidth(),
167
                $box->getInnerLength(),
168
                $box->getInnerDepth()
169
            );
170
            static::$emptyBoxCache[$cacheKey] = $orientations;
171
        }
172
173
        return $orientations;
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
    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
        $stableOrientations = [];
194
        $unstableOrientations = [];
195
196
        foreach ($possibleOrientations as $o => $orientation) {
197
            if ($orientation->isStable()) {
198
                $stableOrientations[] = $orientation;
199
            } else {
200
                $unstableOrientations[] = $orientation;
201
            }
202
        }
203
204
        $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
        if (count($stableOrientations) > 0) {
212
            $orientationsToUse = $stableOrientations;
213
        } elseif (count($unstableOrientations) > 0) {
214
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
215
216
            $stableOrientationsInEmptyBox = array_filter(
217
                $orientationsInEmptyBox,
218
                function (OrientatedItem $orientation) {
219
                    return $orientation->isStable();
220
                }
221
            );
222
223
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
224
                $orientationsToUse = $unstableOrientations;
225
            }
226
        }
227
228
        return $orientationsToUse;
229
    }
230
}
231