Passed
Push — 3.x ( 0d7474...922c95 )
by Doug
15:19 queued 14:00
created

OrientatedItemFactory::getPossibleOrientations()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 7.9936

Importance

Changes 10
Bugs 1 Features 0
Metric Value
eloc 9
dl 0
loc 28
ccs 8
cts 11
cp 0.7272
c 10
b 1
f 0
rs 8.8333
cc 7
nc 6
nop 9
crap 7.9936

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 OrientatedItem[]
40
     */
41
    protected static $emptyBoxCache = [];
42
43 64
    public function __construct(Box $box)
44
    {
45 64
        $this->box = $box;
46 64
        $this->logger = new NullLogger();
47 64
    }
48
49 41
    public function setSinglePassMode(bool $singlePassMode): void
50
    {
51 41
        $this->singlePassMode = $singlePassMode;
52 41
    }
53
54
    /**
55
     * Get the best orientation for an item.
56
     */
57 62
    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
    ): ?OrientatedItem {
70 62
        $this->logger->debug(
71 62
            "evaluating item {$item->getDescription()} for fit",
72
            [
73 62
                'item' => $item,
74
                'space' => [
75 62
                    'widthLeft' => $widthLeft,
76 62
                    'lengthLeft' => $lengthLeft,
77 62
                    'depthLeft' => $depthLeft,
78
                ],
79
            ]
80
        );
81
82 62
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList);
83 62
        $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations);
84
85 62
        if (empty($usableOrientations)) {
86 58
            return null;
87
        }
88
89 62
        $sorter = new OrientatedItemSorter($this, $this->singlePassMode, $widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList);
90 62
        $sorter->setLogger($this->logger);
91 62
        usort($usableOrientations, $sorter);
92
93 62
        $this->logger->debug('Selected best fit orientation', ['orientation' => $usableOrientations[0]]);
94
95 62
        return $usableOrientations[0];
96
    }
97
98
    /**
99
     * Find all possible orientations for an item.
100
     *
101
     * @return OrientatedItem[]
102
     */
103 62
    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 62
        $permutations = $this->generatePermutations($item, $prevItem);
115
116
        //remove any that simply don't fit
117 62
        $orientations = [];
118 62
        foreach ($permutations as $dimensions) {
119 62
            if ($dimensions[0] <= $widthLeft && $dimensions[1] <= $lengthLeft && $dimensions[2] <= $depthLeft) {
120 62
                $orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
121
            }
122
        }
123
124 62
        if ($item instanceof ConstrainedPlacementItem && !$this->box instanceof WorkingVolume) {
125
            $orientations = array_filter($orientations, function (OrientatedItem $i) use ($x, $y, $z, $prevPackedItemList) {
126
                return $i->getItem()->canBePacked($this->box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth());
127
            });
128
        }
129
130 62
        return $orientations;
131
    }
132
133
    /**
134
     * @return OrientatedItem[]
135
     */
136 19
    public function getPossibleOrientationsInEmptyBox(Item $item): array
137
    {
138 19
        $cacheKey = $item->getWidth() .
139 19
            '|' .
140 19
            $item->getLength() .
141 19
            '|' .
142 19
            $item->getDepth() .
143 19
            '|' .
144 19
            ($item->getKeepFlat() ? '2D' : '3D') .
145 19
            '|' .
146 19
            $this->box->getInnerWidth() .
147 19
            '|' .
148 19
            $this->box->getInnerLength() .
149 19
            '|' .
150 19
            $this->box->getInnerDepth();
151
152 19
        if (isset(static::$emptyBoxCache[$cacheKey])) {
153 18
            $orientations = static::$emptyBoxCache[$cacheKey];
154
        } else {
155 12
            $orientations = $this->getPossibleOrientations(
156 12
                $item,
157 12
                null,
158 12
                $this->box->getInnerWidth(),
159 12
                $this->box->getInnerLength(),
160 12
                $this->box->getInnerDepth(),
161 12
                0,
162 12
                0,
163 12
                0,
164 12
                new PackedItemList()
165
            );
166 12
            static::$emptyBoxCache[$cacheKey] = $orientations;
167
        }
168
169 19
        return $orientations;
170
    }
171
172
    /**
173
     * @param  OrientatedItem[] $possibleOrientations
174
     * @return OrientatedItem[]
175
     */
176 62
    protected function getUsableOrientations(
177
        Item $item,
178
        array $possibleOrientations
179
    ): array {
180 62
        $orientationsToUse = $stableOrientations = $unstableOrientations = [];
181
182
        // Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
183 62
        foreach ($possibleOrientations as $orientation) {
184 62
            if ($orientation->isStable() || $this->box->getInnerDepth() === $orientation->getDepth()) {
185 62
                $stableOrientations[] = $orientation;
186
            } else {
187
                $unstableOrientations[] = $orientation;
188
            }
189
        }
190
191
        /*
192
         * We prefer to use stable orientations only, but allow unstable ones if
193
         * the item doesn't fit in the box any other way
194
         */
195 62
        if (count($stableOrientations) > 0) {
196 62
            $orientationsToUse = $stableOrientations;
197 58
        } elseif (count($unstableOrientations) > 0) {
198
            $stableOrientationsInEmptyBox = $this->getStableOrientationsInEmptyBox($item);
199
200
            if (count($stableOrientationsInEmptyBox) === 0) {
201
                $orientationsToUse = $unstableOrientations;
202
            }
203
        }
204
205 62
        return $orientationsToUse;
206
    }
207
208
    /**
209
     * Return the orientations for this item if it were to be placed into the box with nothing else.
210
     */
211
    protected function getStableOrientationsInEmptyBox(Item $item): array
212
    {
213
        $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item);
214
215
        return array_filter(
216
            $orientationsInEmptyBox,
217
            function (OrientatedItem $orientation) {
218
                return $orientation->isStable();
219
            }
220
        );
221
    }
222
223 62
    private function generatePermutations(Item $item, ?OrientatedItem $prevItem): array
224
    {
225 62
        $permutations = [];
226
227
        //Special case items that are the same as what we just packed - keep orientation
228 62
        if ($prevItem && $prevItem->isSameDimensions($item)) {
229 50
            $permutations[] = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
230
        } else {
231
            //simple 2D rotation
232 62
            $permutations[] = [$item->getWidth(), $item->getLength(), $item->getDepth()];
233 62
            $permutations[] = [$item->getLength(), $item->getWidth(), $item->getDepth()];
234
235
            //add 3D rotation if we're allowed
236 62
            if (!$item->getKeepFlat()) {
237 38
                $permutations[] = [$item->getWidth(), $item->getDepth(), $item->getLength()];
238 38
                $permutations[] = [$item->getLength(), $item->getDepth(), $item->getWidth()];
239 38
                $permutations[] = [$item->getDepth(), $item->getWidth(), $item->getLength()];
240 38
                $permutations[] = [$item->getDepth(), $item->getLength(), $item->getWidth()];
241
            }
242
        }
243
244 62
        return $permutations;
245
    }
246
}
247