Passed
Push — 3.x ( 05eab4...5fb647 )
by Doug
05:29 queued 03:46
created

OrientatedItemFactory   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 215
Duplicated Lines 0 %

Test Coverage

Coverage 59.14%

Importance

Changes 31
Bugs 3 Features 0
Metric Value
eloc 85
c 31
b 3
f 0
dl 0
loc 215
ccs 55
cts 93
cp 0.5914
rs 10
wmc 26

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A setSinglePassMode() 0 3 1
A getBestOrientation() 0 40 3
B getPossibleOrientations() 0 28 7
A generatePermutations() 0 25 4
A hasStableOrientationsInEmptyBox() 0 41 3
B getUsableOrientations() 0 28 7
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 bool[]
40
     */
41
    protected static $emptyBoxStableItemOrientationCache = [];
42
43 84
    public function __construct(Box $box)
44
    {
45 84
        $this->box = $box;
46 84
        $this->logger = new NullLogger();
47 84
    }
48
49 26
    public function setSinglePassMode(bool $singlePassMode): void
50
    {
51 26
        $this->singlePassMode = $singlePassMode;
52 26
    }
53
54
    /**
55
     * Get the best orientation for an item.
56
     */
57 82
    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 82
        $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 82
            "evaluating item {$item->getDescription()} for fit",
73
            [
74 82
                'item' => $item,
75
                'space' => [
76 82
                    'widthLeft' => $widthLeft,
77 82
                    'lengthLeft' => $lengthLeft,
78 82
                    'depthLeft' => $depthLeft,
79
                ],
80
            ]
81
        );
82
83 82
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
84 82
        $usableOrientations = $considerStability ? $this->getUsableOrientations($item, $possibleOrientations) : $possibleOrientations;
85
86 82
        if (empty($usableOrientations)) {
87 74
            return null;
88
        }
89
90 82
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList);
91 82
        $sorter->setLogger($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\OrientatedItemSorter::setLogger() 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

91
        $sorter->setLogger(/** @scrutinizer ignore-type */ $this->logger);
Loading history...
92 82
        usort($usableOrientations, $sorter);
93
94 82
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
95
96 82
        return $usableOrientations[0];
97
    }
98
99
    /**
100
     * Find all possible orientations for an item.
101
     *
102
     * @return OrientatedItem[]
103
     */
104 84
    public function getPossibleOrientations(
105
        Item $item,
106
        ?OrientatedItem $prevItem,
107
        int $widthLeft,
108
        int $lengthLeft,
109
        int $depthLeft,
110
        int $x,
111
        int $y,
112
        int $z,
113
        PackedItemList $prevPackedItemList
114
    ): array {
115 84
        $permutations = $this->generatePermutations($item, $prevItem);
116
117
        //remove any that simply don't fit
118 84
        $orientations = [];
119 84
        foreach ($permutations as $dimensions) {
120 84
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
121 82
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
122
            }
123
        }
124
125 84
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
126
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
127
                return $i->getItem()->canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
128
            });
129
        }
130
131 84
        return $orientations;
132
    }
133
134
    /**
135
     * @param  OrientatedItem[] $possibleOrientations
136
     * @return OrientatedItem[]
137
     */
138 82
    protected function getUsableOrientations(
139
        Item $item,
140
        array $possibleOrientations
141
    ): array {
142 82
        $stableOrientations = $unstableOrientations = [];
143
144
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
145 82
        foreach ($possibleOrientations as $orientation) {
146 82
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
147 82
                $stableOrientations[] = $orientation;
148
            } else {
149
                $unstableOrientations[] = $orientation;
150
            }
151
        }
152
153
        /*
154
         * We prefer to use stable orientations only, but allow unstable ones if
155
         * the item doesn't fit in the box any other way
156
         */
157 82
        if (count($stableOrientations) > 0) {
158 82
            return $stableOrientations;
159
        }
160
161 74
        if ((count($unstableOrientations) > 0) && !$this->hasStableOrientationsInEmptyBox($item)) {
162
            return $unstableOrientations;
163
        }
164
165 74
        return [];
166
    }
167
168
    /**
169
     * Return the orientations for this item if it were to be placed into the box with nothing else.
170
     */
171
    protected function hasStableOrientationsInEmptyBox(Item $item): bool
172
    {
173
        $cacheKey = $item->getWidth() .
174
            '|' .
175
            $item->getLength() .
176
            '|' .
177
            $item->getDepth() .
178
            '|' .
179
            ($item->getKeepFlat() ? '2D' : '3D') .
180
            '|' .
181
            $this->box->getInnerWidth() .
182
            '|' .
183
            $this->box->getInnerLength() .
184
            '|' .
185
            $this->box->getInnerDepth();
186
187
        if (isset(static::$emptyBoxStableItemOrientationCache[$cacheKey])) {
188
            return static::$emptyBoxStableItemOrientationCache[$cacheKey];
189
        }
190
191
        $orientations = $this->getPossibleOrientations(
192
            $item,
193
            null,
194
            $this->box->getInnerWidth(),
195
            $this->box->getInnerLength(),
196
            $this->box->getInnerDepth(),
197
            0,
198
            0,
199
            0,
200
            new PackedItemList()
201
        );
202
203
        $stableOrientations = array_filter(
204
            $orientations,
205
            static function (OrientatedItem $orientation) {
206
                return $orientation->isStable();
207
            }
208
        );
209
        static::$emptyBoxStableItemOrientationCache[$cacheKey] = count($stableOrientations) > 0;
210
211
        return static::$emptyBoxStableItemOrientationCache[$cacheKey];
212
    }
213
214 84
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
215
    {
216
        //Special case items that are the same as what we just packed - keep orientation
217 84
        if ($prevItem && $prevItem->isSameDimensions($item)) {
218 62
            return [[$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()]];
219
        }
220
221 84
        $permutations = [];
222 84
        $w = $item->getWidth();
223 84
        $l = $item->getLength();
224 84
        $d = $item->getDepth();
225
226
        //simple 2D rotation
227 84
        $permutations[$w . '|' . $l . '|' . $d] = [$w, $l, $d];
228 84
        $permutations[$l . '|' . $w . '|' . $d] = [$l, $w, $d];
229
230
        //add 3D rotation if we're allowed
231 84
        if (!$item->getKeepFlat()) {
232 52
            $permutations[$w . '|' . $d . '|' . $l] = [$w, $d, $l];
233 52
            $permutations[$l . '|' . $d . '|' . $w] = [$l, $d, $w];
234 52
            $permutations[$d . '|' . $w . '|' . $l] = [$d, $w, $l];
235 52
            $permutations[$d . '|' . $l . '|' . $w] = [$d, $l, $w];
236
        }
237
238 84
        return $permutations;
239
    }
240
}
241