Passed
Push — 3.x ( d739e8...b351ea )
by Doug
03:11
created

LayerPacker::packLayer()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 82
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 13

Importance

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

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