Passed
Push — 3.x ( 922c95...5b6501 )
by Doug
12:25 queued 11: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 10
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 73
    public function __construct(Box $box)
57
    {
58 73
        $this->box = $box;
59 73
        $this->logger = new NullLogger();
60
61 73
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
62 73
        $this->orientatedItemFactory->setLogger($this->logger);
63 73
    }
64
65
    /**
66
     * Sets a logger.
67
     */
68 73
    public function setLogger(LoggerInterface $logger): void
69
    {
70 73
        $this->logger = $logger;
71 73
        $this->orientatedItemFactory->setLogger($logger);
72 73
    }
73
74 53
    public function setSinglePassMode(bool $singlePassMode): void
75
    {
76 53
        $this->singlePassMode = $singlePassMode;
77 53
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
78 53
    }
79
80
    /**
81
     * Pack items into an individual vertical layer.
82
     */
83 73
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, array $layers, int $z, int $layerWidth, int $lengthLeft, int $depthLeft, int $guidelineLayerDepth): PackedLayer
84
    {
85 73
        $layer = new PackedLayer();
86 73
        $prevItem = null;
87 73
        $x = $y = $rowLength = 0;
88 73
        $skippedItems = [];
89 73
        $remainingWeightAllowed = $this->getRemainingWeightAllowed($layers);
90
91 73
        while ($items->count() > 0) {
92 73
            $itemToPack = $items->extract();
93
94
            //skip items that will never fit e.g. too heavy
95 73
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
96 9
                continue;
97
            }
98
99 73
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $layerWidth - $x, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, $packedItemList);
100
101 73
            if ($orientatedItem instanceof OrientatedItem) {
102 73
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
103 73
                $layer->insert($packedItem);
104 73
                $remainingWeightAllowed -= $itemToPack->getWeight();
105 73
                $packedItemList->insert($packedItem);
106
107 73
                $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 73
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z);
112 73
                $x += $packedItem->getWidth();
113
114 73
                $prevItem = $orientatedItem;
115 73
                if ($items->count() === 0) {
116 68
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
117 68
                    $skippedItems = [];
118
                }
119 73
                continue;
120
            }
121
122 65
            if ($items->count() > 0) { // skip for now, move on to the next item
123 53
                $this->logger->debug("doesn't fit, skipping for now");
124 53
                $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 53
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
127 24
                    $skippedItems[] = $items->extract();
128
                }
129 53
                continue;
130
            }
131
132 65
            if ($x > 0) {
133 64
                $this->logger->debug('No more fit in width wise, resetting for new row');
134 64
                $lengthLeft -= $rowLength;
135 64
                $y += $rowLength;
136 64
                $x = $rowLength = 0;
137 64
                $skippedItems[] = $itemToPack;
138 64
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
139 64
                $skippedItems = [];
140 64
                $prevItem = null;
141 64
                continue;
142
            }
143
144 54
            $this->logger->debug('no items fit, so starting next vertical layer');
145 54
            $skippedItems[] = $itemToPack;
146
147 54
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
148
149 54
            return $layer;
150
        }
151
152 71
        return $layer;
153
    }
154
155 73
    private function packVerticallyInsideItemFootprint(PackedLayer $layer, PackedItem $packedItem, PackedItemList $packedItemList, ItemList &$items, int &$remainingWeightAllowed, int $guidelineLayerDepth, int $rowLength, int $x, int $y, int $z): void
156
    {
157 73
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
158 73
        $stackedZ = $z + $packedItem->getDepth();
159 73
        $stackSkippedItems = [];
160 73
        $stackedItem = $packedItem->toOrientatedItem();
161 73
        while ($stackableDepth > 0 && $items->count() > 0) {
162 19
            $itemToTryStacking = $items->extract();
163
164
            //skip items that will never fit
165 19
            if (!$this->checkNonDimensionalConstraints($itemToTryStacking, $remainingWeightAllowed, $packedItemList)) {
166
                continue;
167
            }
168
169 19
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList);
170 19
            if ($stackedItem) {
171 13
                $packedStackedItem = PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ);
172 13
                $layer->insert($packedStackedItem);
173 13
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
174 13
                $packedItemList->insert($packedStackedItem);
175 13
                $stackableDepth -= $stackedItem->getDepth();
176 13
                $stackedZ += $stackedItem->getDepth();
177 13
                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 73
        $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
187 73
    }
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 73
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
194
    {
195 73
        $customConstraintsOK = true;
196 73
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
197 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
198
        }
199
200 73
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
201
    }
202
203 73
    private function getRemainingWeightAllowed(array $layers): int
204
    {
205 73
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
206 73
        foreach ($layers as $layer) {
207 53
            $remainingWeightAllowed -= $layer->getWeight();
208
        }
209
210 73
        return $remainingWeightAllowed;
211
    }
212
213
    /**
214
     * Compare two items to see if they have same dimensions.
215
     */
216 33
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
217
    {
218 33
        if ($itemA === $itemB) {
219 22
            return true;
220
        }
221 15
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
222 15
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
223 15
        sort($itemADimensions);
224 15
        sort($itemBDimensions);
225
226 15
        return $itemADimensions === $itemBDimensions;
227
    }
228
}
229