Passed
Pull Request — master (#246)
by
unknown
18:54
created

OrientatedItemFactory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
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 function array_filter;
12
use function count;
13
use Psr\Log\LoggerAwareInterface;
14
use Psr\Log\LoggerAwareTrait;
15
use Psr\Log\NullLogger;
16
use function usort;
17
18
/**
19
 * Figure out orientations for an item and a given set of dimensions.
20
 *
21
 * @author Doug Wright
22
 * @internal
23
 */
24
class OrientatedItemFactory implements LoggerAwareInterface
25
{
26
    use LoggerAwareTrait;
27
28
    /** @var Box */
29
    protected $box;
30
31
    /**
32
     * Whether the packer is in single-pass mode.
33
     *
34
     * @var bool
35
     */
36
    protected $singlePassMode = false;
37
38
    /**
39
     * @var array<string, array<OrientatedItem>>
40
     */
41
    protected static $emptyBoxCache = [];
42
43 78
    public function __construct(Box $box)
44
    {
45 78
        $this->box = $box;
46 78
        $this->logger = new NullLogger();
47 78
    }
48
49 58
    public function setSinglePassMode(bool $singlePassMode): void
50
    {
51 58
        $this->singlePassMode = $singlePassMode;
52 58
    }
53
54
    /**
55
     * Get the best orientation for an item.
56
     */
57 70
    public function getBestOrientation(
58
        Item $item,
59
        ?OrientatedItem $prevItem,
60
        ItemList $nextItems,
61
        int $widthLeft,
62
        int $lengthLeft,
63
        int $depthLeft,
64
        int $rowLength,
65
        int $x,
66
        int $y,
67
        int $z,
68
        PackedItemList $prevPackedItemList,
69
        bool $considerStability
70
    ): ?OrientatedItem {
71 70
        $this->logger->debug(
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

71
        $this->logger->/** @scrutinizer ignore-call */ 
72
                       debug(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
72 70
            "evaluating item {$item->getDescription()} for fit",
73
            [
74 70
                'item' => $item,
75
                'space' => [
76 70
                    'widthLeft' => $widthLeft,
77 70
                    'lengthLeft' => $lengthLeft,
78 70
                    'depthLeft' => $depthLeft,
79
                ],
80
            ]
81
        );
82
83 70
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
84 70
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
85
86 70
        if (empty($usableOrientations)) {
87 62
            return null;
88
        }
89
90 70
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, $this->logger);
0 ignored issues
show
Bug introduced by
It seems like $this->logger can also be of type null; however, parameter $logger of DVDoug\BoxPacker\Orienta...emSorter::__construct() does only seem to accept Psr\Log\LoggerInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList, /** @scrutinizer ignore-type */ $this->logger);
Loading history...
91 70
        usort($usableOrientations, $sorter);
92
93 70
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
94
95 70
        return $usableOrientations[0];
96
    }
97
98
    /**
99
     * Find all possible orientations for an item.
100
     *
101
     * @return OrientatedItem[]
102
     */
103 76
    public function getPossibleOrientations(
104
        Item $item,
105
        ?OrientatedItem $prevItem,
106
        int $widthLeft,
107
        int $lengthLeft,
108
        int $depthLeft,
109
        int $x,
110
        int $y,
111
        int $z,
112
        PackedItemList $prevPackedItemList
113
    ): array {
114 76
        $permutations = $this->generatePermutations($item, $prevItem);
115
116
        //remove any that simply don't fit
117 76
        $orientations = [];
118 76
        foreach ($permutations as $dimensions) {
119 76
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
120 76
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
121
            }
122
        }
123
124 76
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
125 6
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
126
                /** @var ConstrainedPlacementItem */
127 6
                $constrainedItem = $i->getItem();
128
129 6
                return $constrainedItem->canBePacked(new PackedBox($this->box, $prevPackedItemList), $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
0 ignored issues
show
Bug introduced by
The method canBePacked() does not exist on DVDoug\BoxPacker\Item. It seems like you code against a sub-type of said class. However, the method does not exist in DVDoug\BoxPacker\Test\TestItem. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

129
                return $constrainedItem->/** @scrutinizer ignore-call */ canBePacked(new PackedBox($this->box, $prevPackedItemList), $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
Loading history...
130 6
            });
131
        }
132
133 76
        return $orientations;
134
    }
135
136
    /**
137
     * @return OrientatedItem[]
138
     */
139 54
    public function getPossibleOrientationsInEmptyBox(Item $item): array
140
    {
141 54
        $cacheKey = $item->getWidth() .
142 54
            '|' .
143 54
            $item->getLength() .
144 54
            '|' .
145 54
            $item->getDepth() .
146 54
            '|' .
147 54
            $item->getAllowedRotations() .
148 54
            '|' .
149 54
            $this->box->getInnerWidth() .
150 54
            '|' .
151 54
            $this->box->getInnerLength() .
152 54
            '|' .
153 54
            $this->box->getInnerDepth();
154
155 54
        if (isset(static::$emptyBoxCache[$cacheKey])) {
156 38
            $orientations = static::$emptyBoxCache[$cacheKey];
157
        } else {
158 48
            $orientations = $this->getPossibleOrientations(
159 48
                $item,
160 48
                null,
161 48
                $this->box->getInnerWidth(),
162 48
                $this->box->getInnerLength(),
163 48
                $this->box->getInnerDepth(),
164 48
                0,
165 48
                0,
166 48
                0,
167 48
                new PackedItemList()
168
            );
169 48
            static::$emptyBoxCache[$cacheKey] = $orientations;
170
        }
171
172 54
        return $orientations;
173
    }
174
175
    /**
176
     * @param  OrientatedItem[] $possibleOrientations
177
     * @return OrientatedItem[]
178
     */
179 70
    protected function getUsableOrientations(
180
        Item $item,
181
        array $possibleOrientations
182
    ): array {
183 70
        $stableOrientations = $unstableOrientations = [];
184
185
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
186 70
        foreach ($possibleOrientations as $orientation) {
187 70
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
188 66
                $stableOrientations[] = $orientation;
189
            } else {
190 18
                $unstableOrientations[] = $orientation;
191
            }
192
        }
193
194
        /*
195
         * We prefer to use stable orientations only, but allow unstable ones if
196
         * the item doesn't fit in the box any other way
197
         */
198 70
        if (count($stableOrientations) > 0) {
199 66
            return $stableOrientations;
200
        }
201
202 62
        if (count($unstableOrientations) > 0) {
203 16
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
204
205 16
            if (count($stableOrientationsInEmptyBox) === 0) {
206 8
                return $unstableOrientations;
207
            }
208
        }
209
210 62
        return [];
211
    }
212
213
    /**
214
     * Return the orientations for this item if it were to be placed into the box with nothing else.
215
     * @return OrientatedItem[]
216
     */
217 16
    protected function getStableOrientationsInEmptyBox(Item $item): array
218
    {
219 16
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
220
221 16
        return array_filter(
222 16
            $orientationsInEmptyBox,
223 16
            function (OrientatedItem $orientation) {
224 16
                return $orientation->isStable();
225 16
            }
226
        );
227
    }
228
229
    /**
230
     * @return array<array<int>>
231
     */
232 76
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
233
    {
234
        //Special case items that are the same as what we just packed - keep orientation
235 76
        if ($prevItem && $prevItem->isSameDimensions($item)) {
236 54
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
237
        }
238
239 76
        $permutations = [];
240 76
        $w = $item->getWidth();
241 76
        $l = $item->getLength();
242 76
        $d = $item->getDepth();
243
244 76
        $permutations[$w . $l . $d] = [$w, $l, $d];
245
246 76
        if ($item->getAllowedRotations() > 1) { //simple 2D rotation
247 74
            $permutations[$l . $w . $d] = [$l, $w, $d];
248
        }
249
250 76
        if ($item->getAllowedRotations() === Item::ROTATION_BEST_FIT) { //add 3D rotation if we're allowed
251 54
            $permutations[$w . $d . $l] = [$w, $d, $l];
252 54
            $permutations[$l . $d . $w] = [$l, $d, $w];
253 54
            $permutations[$d . $w . $l] = [$d, $w, $l];
254 54
            $permutations[$d . $l . $w] = [$d, $l, $w];
255
        }
256
257 76
        return $permutations;
258
    }
259
260
    /**
261
     * Clear the empty box cache. Useful is part of a long running 
262
     * process to prevent memory leaks.
263
     */
264
    public static function clearEmptyBoxCache() : void
265
    {
266
        self::$emptyBoxCache = [];
267
    }
268
}
269