Completed
Push — refactor_layerpacker ( dfc43c )
by Doug
09:17
created

LayerPacker::packLayer()   D

Complexity

Conditions 20
Paths 23

Size

Total Lines 90
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 63
CRAP Score 20

Importance

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