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