Passed
Push — private_sort ( d01cd6 )
by Doug
07:29
created

OrientatedItemFactory::getBestOrientation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 39
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 2

Importance

Changes 14
Bugs 3 Features 0
Metric Value
cc 2
eloc 16
c 14
b 3
f 0
nc 2
nop 11
dl 0
loc 39
ccs 16
cts 16
cp 1
crap 2
rs 9.7333

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