Passed
Push — master ( 34b8f8...cb2e0a )
by Doug
05:48
created

LayerPacker::packLayer()   D

Complexity

Conditions 20
Paths 23

Size

Total Lines 89
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 62
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 20
eloc 63
c 1
b 0
f 0
nc 23
nop 8
dl 0
loc 89
ccs 62
cts 62
cp 1
crap 20
rs 4.1666

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, $orientatedItem->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
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $orientatedItem->getDepth();
110 56
                $stackedZ = $z + $orientatedItem->getDepth();
111 56
                $stackSkippedItems = [];
112 56
                while ($stackableDepth > 0 && $items->count() > 0) {
113 17
                    $itemToTryStacking = $items->extract();
114 17
                    $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $prevItem instanceof PackedItem ? $prevItem->toOrientatedItem() : null, $items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList);
115 17
                    if ($stackedItem && $this->checkNonDimensionalConstraints($itemToTryStacking, $remainingWeightAllowed, $packedItemList)) {
116 11
                        $layer->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ));
117 11
                        $remainingWeightAllowed -= $itemToTryStacking->getWeight();
118 11
                        $packedItemList->insert($packedItem);
119 11
                        $stackableDepth -= $stackedItem->getDepth();
120 11
                        $stackedZ += $stackedItem->getDepth();
121
                    } else {
122 13
                        $stackSkippedItems[] = $itemToTryStacking;
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 13
                        while ($items->count() > 0 && static::isSameDimensions($itemToTryStacking, $items->top())) {
125 10
                            $stackSkippedItems[] = $items->extract();
126
                        }
127
                    }
128
                }
129 56
                if ($stackSkippedItems) {
130 13
                    $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
131
                }
132 56
                $x += $orientatedItem->getWidth();
133
134 56
                $prevItem = $packedItem;
135 56
                if ($items->count() === 0) {
136 53
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
137 56
                    $skippedItems = [];
138
                }
139 50
            } elseif (count($layer->getItems()) === 0) { // zero items on layer
140 39
                $this->logger->debug("doesn't fit on layer even when empty, skipping for good");
141 39
                continue;
142 49
            } elseif ($items->count() > 0) { // skip for now, move on to the next item
143 41
                $this->logger->debug("doesn't fit, skipping for now");
144 41
                $skippedItems[] = $itemToPack;
145
                // 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
146 41
                while ($items->count() > 2 && static::isSameDimensions($itemToPack, $items->top())) {
147 22
                    $skippedItems[] = $items->extract();
148
                }
149 49
            } elseif ($x > 0) {
150 49
                $this->logger->debug('No more fit in width wise, resetting for new row');
151 49
                $lengthLeft -= $rowLength;
152 49
                $y += $rowLength;
153 49
                $x = $rowLength = 0;
154 49
                $skippedItems[] = $itemToPack;
155 49
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
156 49
                $skippedItems = [];
157 49
                $prevItem = null;
158 49
                continue;
159
            } else {
160 44
                $this->logger->debug('no items fit, so starting next vertical layer');
161 44
                $skippedItems[] = $itemToPack;
162
163 44
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
164
165 44
                return $layer;
166
            }
167
        }
168
169 56
        return $layer;
170
    }
171
172
    /**
173
     * As well as purely dimensional constraints, there are other constraints that need to be met
174
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
175
     */
176 56
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
177
    {
178 56
        $customConstraintsOK = true;
179 56
        if ($itemToPack instanceof ConstrainedItem) {
180 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
181
        }
182
183 56
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
184
    }
185
186 56
    private function getRemainingWeightAllowed(array $layers): int
187
    {
188 56
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
189 56
        foreach ($layers as $layer) {
190 44
            $remainingWeightAllowed -= $layer->getWeight();
191
        }
192
193 56
        return $remainingWeightAllowed;
194
    }
195
196
    /**
197
     * Compare two items to see if they have same dimensions.
198
     */
199 26
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
200
    {
201 26
        if ($itemA === $itemB) {
202 19
            return true;
203
        }
204 10
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
205 10
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
206 10
        sort($itemADimensions);
207 10
        sort($itemBDimensions);
208
209 10
        return $itemADimensions === $itemBDimensions;
210
    }
211
}
212