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