Test Failed
Push — 2.x-dev ( 5e90af...eccc16 )
by Doug
03:16
created

LayerPacker::packVerticallyInsideItemFootprint()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 10.3696

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 20
c 1
b 0
f 0
nc 5
nop 10
dl 0
loc 31
ccs 14
cts 21
cp 0.6667
crap 10.3696
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 28
    public function __construct(Box $box)
51
    {
52 28
        $this->box = $box;
53 28
        $this->logger = new NullLogger();
54
55 28
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
56 28
        $this->orientatedItemFactory->setLogger($this->logger);
57 28
    }
58
59
    /**
60
     * Sets a logger.
61
     */
62 28
    public function setLogger(LoggerInterface $logger)
63
    {
64 28
        $this->logger = $logger;
65 28
        $this->orientatedItemFactory->setLogger($logger);
66 28
    }
67
68 23
    public function setSinglePassMode($singlePassMode)
69
    {
70 23
        $this->singlePassMode = $singlePassMode;
71 23
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
72 23
    }
73
74
    /**
75
     * Pack items into an individual vertical layer.
76
     */
77 28
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, array $layers, $z, $layerWidth, $lengthLeft, $depthLeft, $guidelineLayerDepth)
78
    {
79 28
        $layer = new PackedLayer();
80 28
        $prevItem = null;
81 28
        $x = $y = $rowLength = 0;
82 28
        $skippedItems = [];
83 28
        $remainingWeightAllowed = $this->getRemainingWeightAllowed($layers);
84
85 28
        while ($items->count() > 0) {
86 28
            $itemToPack = $items->extract();
87
88
            //skip items that will never fit e.g. too heavy
89 28
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
90 1
                continue;
91
            }
92
93 28
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $layerWidth - $x, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, $packedItemList);
94
95 28
            if ($orientatedItem instanceof OrientatedItem) {
96 18
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
97 18
                $layer->insert($packedItem);
98 18
                $remainingWeightAllowed -= $itemToPack->getWeight();
99 18
                $packedItemList->insert($packedItem);
100
101 18
                $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 18
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z);
106 18
                $x += $packedItem->getWidth();
107
108 18
                $prevItem = $orientatedItem;
109 18
                if ($items->count() === 0) {
110 16
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)));
111 16
                    $skippedItems = [];
112
                }
113 18
                continue;
114
            }
115
116 26
            if ($items->count() > 0) { // skip for now, move on to the next item
117 17
                $this->logger->debug("doesn't fit, skipping for now");
118 17
                $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 17
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
121 5
                    $skippedItems[] = $items->extract();
122
                }
123 17
                continue;
124
            }
125
126 26
            if ($x > 0) {
127 15
                $this->logger->debug('No more fit in width wise, resetting for new row');
128 15
                $lengthLeft -= $rowLength;
129 15
                $y += $rowLength;
130 15
                $x = $rowLength = 0;
131 15
                $skippedItems[] = $itemToPack;
132 15
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)));
133 15
                $skippedItems = [];
134 15
                $prevItem = null;
135 15
                continue;
136
            }
137
138 21
            $this->logger->debug('no items fit, so starting next vertical layer');
139 21
            $skippedItems[] = $itemToPack;
140
141 21
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)));
142
143 21
            return $layer;
144
        }
145
146 15
        return $layer;
147
    }
148
149 18
    private function packVerticallyInsideItemFootprint(PackedLayer $layer, PackedItem $packedItem, PackedItemList $packedItemList, ItemList &$items, &$remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z)
150
    {
151 18
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
152 18
        $stackedZ = $z + $packedItem->getDepth();
153 18
        $stackSkippedItems = [];
154 18
        $stackedItem = $packedItem->toOrientatedItem();
155 18
        while ($stackableDepth > 0 && $items->count() > 0) {
156 2
            $itemToTryStacking = $items->extract();
157
158
            //skip items that will never fit
159 2
            if (!$this->checkNonDimensionalConstraints($itemToTryStacking, $remainingWeightAllowed, $packedItemList)) {
160
                continue;
161
            }
162
163 2
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList);
164 2
            if ($stackedItem) {
165
                $layer->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ));
166
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
167
                $packedItemList->insert($packedItem);
168
                $stackableDepth -= $stackedItem->getDepth();
169
                $stackedZ += $stackedItem->getDepth();
170
                continue;
171
            }
172
173 2
            $stackSkippedItems[] = $itemToTryStacking;
174
            // abandon here if next item is the same, no point trying to keep going
175 2
            while ($items->count() > 0 && static::isSameDimensions($itemToTryStacking, $items->top())) {
176 1
                $stackSkippedItems[] = $items->extract();
177
            }
178
        }
179 18
        $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)));
180 18
    }
181
182
    /**
183
     * As well as purely dimensional constraints, there are other constraints that need to be met
184
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
185
     */
186 28
    private function checkNonDimensionalConstraints(Item $itemToPack, $remainingWeightAllowed, PackedItemList $packedItemList)
187
    {
188 28
        $customConstraintsOK = true;
189 28
        if ($itemToPack instanceof ConstrainedItem) {
190
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList->asItemList(), $this->box);
191
        }
192
193 28
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
194
    }
195
196 28
    private function getRemainingWeightAllowed(array $layers)
197
    {
198 28
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
199 28
        foreach ($layers as $layer) {
200 10
            $remainingWeightAllowed -= $layer->getWeight();
201
        }
202
203 28
        return $remainingWeightAllowed;
204
    }
205
206
    /**
207
     * Compare two items to see if they have same dimensions.
208
     */
209 8
    private static function isSameDimensions(Item $itemA, Item $itemB)
210
    {
211 8
        if ($itemA === $itemB) {
212 5
            return true;
213
        }
214 3
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
215 3
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
216 3
        sort($itemADimensions);
217 3
        sort($itemBDimensions);
218
219 3
        return $itemADimensions === $itemBDimensions;
220
    }
221
}
222