Passed
Push — 3.x ( 97397b...854a27 )
by Doug
12:23 queued 10:33
created

LayerPacker::packLayer()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 82
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 13

Importance

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

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