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