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