Passed
Push — 2.x-dev ( eccc16...ab1c97 )
by Doug
07:42
created

LayerPacker::packVerticallyInsideItemFootprint()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8

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