Passed
Push — 3.x ( 148e8f...9015bf )
by Doug
04:46
created

LayerPacker::packVerticallyInsideItemFootprint()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9.0066

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 9
eloc 22
c 5
b 0
f 0
nc 10
nop 11
dl 0
loc 33
ccs 22
cts 23
cp 0.9565
crap 9.0066
rs 8.0555

2 Methods

Rating   Name   Duplication   Size   Complexity  
A LayerPacker::checkNonDimensionalConstraints() 0 8 4
A LayerPacker::isSameDimensions() 0 11 2

How to fix   Many Parameters   

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