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

LayerPacker::packVerticallyInsideItemFootprint()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8.048

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 21
dl 0
loc 32
ccs 20
cts 22
cp 0.9091
c 4
b 0
f 0
rs 8.4444
cc 8
nc 5
nop 10
crap 8.048

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