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