Passed
Push — 3.x ( 3ac6d2...8ba8a4 )
by Doug
02:05
created

LayerPacker::packVerticallyInsideItemFootprint()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8.006

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 8
eloc 21
c 4
b 0
f 0
nc 5
nop 11
dl 0
loc 32
ccs 21
cts 22
cp 0.9545
crap 8.006
rs 8.4444

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_merge;
12
use function iterator_to_array;
13
use function max;
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\NullLogger;
17
use function sort;
18
19
/**
20
 * Layer packer.
21
 *
22
 * @internal
23
 * @author Doug Wright
24
 */
25
class LayerPacker implements LoggerAwareInterface
26
{
27
    /**
28
     * The logger instance.
29
     *
30
     * @var LoggerInterface
31
     */
32
    private $logger;
33
34
    /**
35
     * Box to pack items into.
36
     *
37
     * @var Box
38
     */
39
    private $box;
40
41
    /**
42
     * Whether the packer is in single-pass mode.
43
     *
44
     * @var bool
45
     */
46
    private $singlePassMode = false;
47
48
    /**
49
     * @var OrientatedItemFactory
50
     */
51
    private $orientatedItemFactory;
52
53
    /**
54
     * Constructor.
55
     */
56 54
    public function __construct(Box $box)
57
    {
58 54
        $this->box = $box;
59 54
        $this->logger = new NullLogger();
60
61 54
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
62 54
        $this->orientatedItemFactory->setLogger($this->logger);
63 54
    }
64
65
    /**
66
     * Sets a logger.
67
     */
68 54
    public function setLogger(LoggerInterface $logger): void
69
    {
70 54
        $this->logger = $logger;
71 54
        $this->orientatedItemFactory->setLogger($logger);
72 54
    }
73
74 49
    public function setSinglePassMode(bool $singlePassMode): void
75
    {
76 49
        $this->singlePassMode = $singlePassMode;
77 49
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
78 49
    }
79
80
    /**
81
     * Pack items into an individual vertical layer.
82
     */
83 54
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, array $layers, int $z, int $layerWidth, int $lengthLeft, int $depthLeft, int $guidelineLayerDepth, bool $considerStability): PackedLayer
84
    {
85 54
        $layer = new PackedLayer();
86 54
        $prevItem = null;
87 54
        $x = $y = $rowLength = 0;
88 54
        $skippedItems = [];
89 54
        $remainingWeightAllowed = $this->getRemainingWeightAllowed($layers);
90
91 54
        while ($items->count() > 0) {
92 54
            $itemToPack = $items->extract();
93
94
            //skip items that will never fit e.g. too heavy
95 54
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
96 7
                continue;
97
            }
98
99 54
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $layerWidth - $x, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
100
101 54
            if ($orientatedItem instanceof OrientatedItem) {
102 54
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
103 54
                $layer->insert($packedItem);
104 54
                $remainingWeightAllowed -= $itemToPack->getWeight();
105 54
                $packedItemList->insert($packedItem);
106
107 54
                $rowLength = max($rowLength, $packedItem->getLength());
108
109
                //Figure out if we can stack the next item vertically on top of this rather than side by side
110
                //e.g. when we've packed a tall item, and have just put a shorter one next to it.
111 54
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z, $considerStability);
112 54
                $x += $packedItem->getWidth();
113
114 54
                $prevItem = $orientatedItem;
115 54
                if ($items->count() === 0) {
116 51
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
117 51
                    $skippedItems = [];
118
                }
119 54
                continue;
120
            }
121
122 48
            if ($items->count() > 0) { // skip for now, move on to the next item
123 39
                $this->logger->debug("doesn't fit, skipping for now");
124 39
                $skippedItems[] = $itemToPack;
125
                // abandon here if next item is the same, no point trying to keep going. Last time is not skipped, need that to trigger appropriate reset logic
126 39
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
127 21
                    $skippedItems[] = $items->extract();
128
                }
129 39
                continue;
130
            }
131
132 48
            if ($x > 0) {
133 47
                $this->logger->debug('No more fit in width wise, resetting for new row');
134 47
                $lengthLeft -= $rowLength;
135 47
                $y += $rowLength;
136 47
                $x = $rowLength = 0;
137 47
                $skippedItems[] = $itemToPack;
138 47
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
139 47
                $skippedItems = [];
140 47
                $prevItem = null;
141 47
                continue;
142
            }
143
144 43
            $this->logger->debug('no items fit, so starting next vertical layer');
145 43
            $skippedItems[] = $itemToPack;
146
147 43
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
148
149 43
            return $layer;
150
        }
151
152 53
        return $layer;
153
    }
154
155 54
    private function packVerticallyInsideItemFootprint(PackedLayer $layer, PackedItem $packedItem, PackedItemList $packedItemList, ItemList &$items, int &$remainingWeightAllowed, int $guidelineLayerDepth, int $rowLength, int $x, int $y, int $z, bool $considerStability): void
156
    {
157 54
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
158 54
        $stackedZ = $z + $packedItem->getDepth();
159 54
        $stackSkippedItems = [];
160 54
        $stackedItem = $packedItem->toOrientatedItem();
161 54
        while ($stackableDepth > 0 && $items->count() > 0) {
162 17
            $itemToTryStacking = $items->extract();
163
164
            //skip items that will never fit
165 17
            if (!$this->checkNonDimensionalConstraints($itemToTryStacking, $remainingWeightAllowed, $packedItemList)) {
166
                continue;
167
            }
168
169 17
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList, $considerStability);
170 17
            if ($stackedItem) {
171 11
                $packedStackedItem = PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ);
172 11
                $layer->insert($packedStackedItem);
173 11
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
174 11
                $packedItemList->insert($packedStackedItem);
175 11
                $stackableDepth -= $stackedItem->getDepth();
176 11
                $stackedZ += $stackedItem->getDepth();
177 11
                continue;
178
            }
179
180 13
            $stackSkippedItems[] = $itemToTryStacking;
181
            // abandon here if next item is the same, no point trying to keep going
182 13
            while ($items->count() > 0 && static::isSameDimensions($itemToTryStacking, $items->top())) {
183 8
                $stackSkippedItems[] = $items->extract();
184
            }
185
        }
186 54
        $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
187 54
    }
188
189
    /**
190
     * As well as purely dimensional constraints, there are other constraints that need to be met
191
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
192
     */
193 54
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
194
    {
195 54
        $customConstraintsOK = true;
196 54
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
197 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
198
        }
199
200 54
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
201
    }
202
203 54
    private function getRemainingWeightAllowed(array $layers): int
204
    {
205 54
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
206 54
        foreach ($layers as $layer) {
207 54
            $remainingWeightAllowed -= $layer->getWeight();
208
        }
209
210 54
        return $remainingWeightAllowed;
211
    }
212
213
    /**
214
     * Compare two items to see if they have same dimensions.
215
     */
216 28
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
217
    {
218 28
        if ($itemA === $itemB) {
219 19
            return true;
220
        }
221 13
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
222 13
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
223 13
        sort($itemADimensions);
224 13
        sort($itemBDimensions);
225
226 13
        return $itemADimensions === $itemBDimensions;
227
    }
228
}
229