Passed
Push — 3.x ( 05eab4...5fb647 )
by Doug
05:29 queued 03:46
created

LayerPacker::packLayer()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 82
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 12

Importance

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