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