Passed
Push — master ( cb2e0a...ce89c2 )
by Doug
05:31
created

LayerPacker::packLayer()   B

Complexity

Conditions 11
Paths 9

Size

Total Lines 67
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 46
c 2
b 0
f 0
nc 9
nop 8
dl 0
loc 67
ccs 46
cts 46
cp 1
crap 11
rs 7.3166

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