Passed
Push — 1.x ( 457fa0...308477 )
by Doug
03:11
created

LayerPacker::packLayer()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 70
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 44
c 1
b 0
f 0
nc 8
nop 8
dl 0
loc 70
ccs 45
cts 45
cp 1
crap 9
rs 7.6604

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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