Passed
Push — master ( 73ddcb...629397 )
by Doug
02:34
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 ($itemToPack->getWeight() > $remainingWeightAllowed) {
93 8
                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 66
            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 66
            if ($x > 0) {
130 65
                $this->logger->debug('No more fit in width wise, resetting for new row');
131 65
                $lengthLeft -= $rowLength;
132 65
                $y += $rowLength;
133 65
                $x = $rowLength = 0;
134 65
                $skippedItems[] = $itemToPack;
135 65
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
136 65
                $skippedItems = [];
137 65
                $prevItem = null;
138 65
                continue;
139
            }
140
141 55
            $this->logger->debug('no items fit, so starting next vertical layer');
142 55
            $skippedItems[] = $itemToPack;
143
144 55
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
145
146 55
            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 ($itemToTryStacking->getWeight() > $remainingWeightAllowed) {
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 73
    private function getRemainingWeightAllowed(array $layers): int
187
    {
188 73
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
189 73
        foreach ($layers as $layer) {
190 54
            $remainingWeightAllowed -= $layer->getWeight();
191
        }
192
193 73
        return $remainingWeightAllowed;
194
    }
195
196
    /**
197
     * Compare two items to see if they have same dimensions.
198
     */
199 33
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
200
    {
201 33
        if ($itemA === $itemB) {
202 22
            return true;
203
        }
204 15
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
205 15
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
206 15
        sort($itemADimensions);
207 15
        sort($itemBDimensions);
208
209 15
        return $itemADimensions === $itemBDimensions;
210
    }
211
}
212