Passed
Push — 3.x ( 8ba8a4...c7e27c )
by Doug
01:45
created

LayerPacker::packLayer()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 82
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 12

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 12
eloc 51
c 5
b 0
f 0
nc 10
nop 10
dl 0
loc 82
ccs 52
cts 52
cp 1
crap 12
rs 6.9666

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 array_merge;
12
use function iterator_to_array;
13
use function max;
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\NullLogger;
17
use function sort;
18
19
/**
20
 * Layer packer.
21
 *
22
 * @internal
23
 * @author Doug Wright
24
 */
25
class LayerPacker implements LoggerAwareInterface
26
{
27
    /**
28
     * The logger instance.
29
     *
30
     * @var LoggerInterface
31
     */
32
    private $logger;
33
34
    /**
35
     * Box to pack items into.
36
     *
37
     * @var Box
38
     */
39
    private $box;
40
41
    /**
42
     * Whether the packer is in single-pass mode.
43
     *
44
     * @var bool
45
     */
46
    private $singlePassMode = false;
47
48
    /**
49
     * @var OrientatedItemFactory
50
     */
51
    private $orientatedItemFactory;
52
53
    /**
54
     * Constructor.
55
     */
56 55
    public function __construct(Box $box)
57
    {
58 55
        $this->box = $box;
59 55
        $this->logger = new NullLogger();
60
61 55
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
62 55
        $this->orientatedItemFactory->setLogger($this->logger);
63 55
    }
64
65
    /**
66
     * Sets a logger.
67
     */
68 55
    public function setLogger(LoggerInterface $logger): void
69
    {
70 55
        $this->logger = $logger;
71 55
        $this->orientatedItemFactory->setLogger($logger);
72 55
    }
73
74 36
    public function setSinglePassMode(bool $singlePassMode): void
75
    {
76 36
        $this->singlePassMode = $singlePassMode;
77 36
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
78 36
    }
79
80
    /**
81
     * Pack items into an individual vertical layer.
82
     */
83 55
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability): PackedLayer
84
    {
85 55
        $layer = new PackedLayer();
86 55
        $x = $startX;
87 55
        $y = $startY;
88 55
        $z = $startZ;
89 55
        $lengthLeft = $lengthForLayer;
90 55
        $rowLength = 0;
91 55
        $prevItem = null;
92 55
        $skippedItems = [];
93 55
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
94
95 55
        while ($items->count() > 0) {
96 55
            $itemToPack = $items->extract();
97
98
            //skip items that will never fit e.g. too heavy
99 55
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
100 7
                continue;
101
            }
102
103 55
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthLeft, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
104
105 55
            if ($orientatedItem instanceof OrientatedItem) {
106 55
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
107 55
                $layer->insert($packedItem);
108 55
                $remainingWeightAllowed -= $itemToPack->getWeight();
109 55
                $packedItemList->insert($packedItem);
110
111 55
                $rowLength = max($rowLength, $packedItem->getLength());
112
113
                //Figure out if we can stack the next item vertically on top of this rather than side by side
114
                //e.g. when we've packed a tall item, and have just put a shorter one next to it.
115 55
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z, $considerStability);
116
117 55
                $prevItem = $orientatedItem;
118
119
                //Having now placed an item, there is space *within the same row* along the length. Pack into that.
120 55
                if (!$this->singlePassMode && $rowLength - $orientatedItem->getLength() > 0) {
121 9
                    $layer->merge($this->packLayer($items, $packedItemList, $x, $y + $orientatedItem->getLength(), $z, $widthForLayer, $rowLength - $orientatedItem->getLength(), $depthForLayer, $layer->getDepth(), $considerStability));
122
                }
123
124 55
                $x += $packedItem->getWidth();
125
126 55
                if ($items->count() === 0 && $skippedItems) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $skippedItems of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
127 6
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
128 6
                    $skippedItems = [];
129
                }
130 55
                continue;
131
            }
132
133 48
            if ($items->count() > 0) { // skip for now, move on to the next item
134 40
                $this->logger->debug("doesn't fit, skipping for now");
135 40
                $skippedItems[] = $itemToPack;
136
                // 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
137 40
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
138 22
                    $skippedItems[] = $items->extract();
139
                }
140 40
                continue;
141
            }
142
143 48
            if ($x > $startX) {
144 48
                $this->logger->debug('No more fit in width wise, resetting for new row');
145 48
                $lengthLeft -= $rowLength;
146 48
                $y += $rowLength;
147 48
                $x = $startX;
148 48
                $rowLength = 0;
149 48
                $skippedItems[] = $itemToPack;
150 48
                $items = ItemList::fromArray($skippedItems, true);
151 48
                $skippedItems = [];
152 48
                $prevItem = null;
153 48
                continue;
154
            }
155
156 40
            $this->logger->debug('no items fit, so starting next vertical layer');
157 40
            $skippedItems[] = $itemToPack;
158
159 40
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
160
161 40
            return $layer;
162
        }
163
164 54
        return $layer;
165
    }
166
167 55
    private function packVerticallyInsideItemFootprint(PackedLayer $layer, PackedItem $packedItem, PackedItemList $packedItemList, ItemList &$items, int &$remainingWeightAllowed, int $guidelineLayerDepth, int $rowLength, int $x, int $y, int $z, bool $considerStability): void
168
    {
169 55
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
170 55
        $stackedZ = $z + $packedItem->getDepth();
171 55
        $stackSkippedItems = [];
172 55
        $stackedItem = $packedItem->toOrientatedItem();
173 55
        while ($stackableDepth > 0 && $items->count() > 0) {
174 17
            $itemToTryStacking = $items->extract();
175
176
            //skip items that will never fit
177 17
            if (!$this->checkNonDimensionalConstraints($itemToTryStacking, $remainingWeightAllowed, $packedItemList)) {
178
                continue;
179
            }
180
181 17
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList, $considerStability);
182 17
            if ($stackedItem) {
183 11
                $packedStackedItem = PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ);
184 11
                $layer->insert($packedStackedItem);
185 11
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
186 11
                $packedItemList->insert($packedStackedItem);
187 11
                $stackableDepth -= $stackedItem->getDepth();
188 11
                $stackedZ += $stackedItem->getDepth();
189 11
                continue;
190
            }
191
192 13
            $stackSkippedItems[] = $itemToTryStacking;
193
            // abandon here if next item is the same, no point trying to keep going
194 13
            while ($items->count() > 0 && static::isSameDimensions($itemToTryStacking, $items->top())) {
195 8
                $stackSkippedItems[] = $items->extract();
196
            }
197
        }
198 55
        if ($stackSkippedItems) {
199 13
            $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
200
        }
201 55
    }
202
203
    /**
204
     * As well as purely dimensional constraints, there are other constraints that need to be met
205
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
206
     */
207 55
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
208
    {
209 55
        $customConstraintsOK = true;
210 55
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
211 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
212
        }
213
214 55
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
215
    }
216
217
    /**
218
     * Compare two items to see if they have same dimensions.
219
     */
220 29
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
221
    {
222 29
        if ($itemA === $itemB) {
223 20
            return true;
224
        }
225 13
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
226 13
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
227 13
        sort($itemADimensions);
228 13
        sort($itemBDimensions);
229
230 13
        return $itemADimensions === $itemBDimensions;
231
    }
232
}
233