LayerPacker::packLayer()   C
last analyzed

Complexity

Conditions 15
Paths 18

Size

Total Lines 87
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 54
CRAP Score 15

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 15
eloc 55
c 6
b 0
f 0
nc 18
nop 11
dl 0
loc 87
ccs 54
cts 54
cp 1
crap 15
rs 5.9166

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
/**
4
 * Box packing (3D bin packing, knapsack problem).
5
 *
6
 * @author Doug Wright
7
 */
8
declare(strict_types=1);
9
10
namespace DVDoug\BoxPacker;
11
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\NullLogger;
15
16
use function array_merge;
17
use function iterator_to_array;
18
use function max;
19
use function sort;
20
21
/**
22
 * Layer packer.
23
 * @internal
24
 */
25
class LayerPacker implements LoggerAwareInterface
26
{
27
    private LoggerInterface $logger;
28
29
    private bool $singlePassMode = false;
30
31
    private readonly OrientatedItemFactory $orientatedItemFactory;
32
33
    private bool $beStrictAboutItemOrdering = false;
34
35
    private bool $isBoxRotated = false;
36 101
37
    public function __construct(private readonly Box $box)
38 101
    {
39
        $this->logger = new NullLogger();
40 101
41 101
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
0 ignored issues
show
Bug introduced by
The property orientatedItemFactory is declared read-only in DVDoug\BoxPacker\LayerPacker.
Loading history...
42
        $this->orientatedItemFactory->setLogger($this->logger);
43
    }
44
45
    /**
46
     * Sets a logger.
47 101
     */
48
    public function setLogger(LoggerInterface $logger): void
49 101
    {
50 101
        $this->logger = $logger;
51
        $this->orientatedItemFactory->setLogger($logger);
52
    }
53 19
54
    public function setSinglePassMode(bool $singlePassMode): void
55 19
    {
56 19
        $this->singlePassMode = $singlePassMode;
57
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
58
    }
59 101
60
    public function setBoxIsRotated(bool $boxIsRotated): void
61 101
    {
62 101
        $this->isBoxRotated = $boxIsRotated;
63
        $this->orientatedItemFactory->setBoxIsRotated($boxIsRotated);
64
    }
65 42
66
    public function beStrictAboutItemOrdering(bool $beStrict): void
67 42
    {
68
        $this->beStrictAboutItemOrdering = $beStrict;
69
    }
70
71
    /**
72
     * Pack items into an individual vertical layer.
73 101
     */
74
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability, ?OrientatedItem $firstItem): PackedLayer
75 101
    {
76 101
        $layer = new PackedLayer();
77 101
        $x = $startX;
78 101
        $y = $startY;
79 101
        $z = $startZ;
80 101
        $rowLength = 0;
81 101
        $prevItem = null;
82 101
        $skippedItems = [];
83
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
84 101
85 101
        while ($items->count() > 0) {
86
            $itemToPack = $items->extract();
87
88 101
            // skip items that will never fit e.g. too heavy
89 18
            if ($itemToPack->getWeight() > $remainingWeightAllowed) {
90
                continue;
91
            }
92 99
93 92
            if ($firstItem instanceof OrientatedItem && $firstItem->item === $itemToPack) {
94 92
                $orientatedItem = $firstItem;
95
                $firstItem = null;
96 94
            } else {
97
                $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
98
            }
99 99
100 98
            if ($orientatedItem instanceof OrientatedItem) {
101 98
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
102 98
                $layer->insert($packedItem);
103
                $packedItemList->insert($packedItem);
104 98
105 98
                $rowLength = max($rowLength, $packedItem->length);
106
                $prevItem = $orientatedItem;
107
108
                // Figure out if we can stack items on top of this rather than side by side
109 98
                // e.g. when we've packed a tall item, and have just put a shorter one next to it.
110 98
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->depth;
111 33
                if ($stackableDepth > 0) {
112 33
                    $stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->depth, $x + $packedItem->width, $y + $packedItem->length, $stackableDepth, $stackableDepth, $considerStability, null);
113
                    $layer->merge($stackedLayer);
114
                }
115 98
116 98
                $x += $packedItem->width;
117
                $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items
118
119 98
                // might be space available lengthwise across the width of this item, up to the current layer length
120
                $layer->merge($this->packLayer($items, $packedItemList, $x - $packedItem->width, $y + $packedItem->length, $z, $x, $y + $rowLength, $depthForLayer, $layer->getDepth(), $considerStability, null));
121 98
122 24
                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...
123 24
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
124
                    $skippedItems = [];
125
                }
126 98
127
                continue;
128
            }
129 94
130 81
            if (!$this->beStrictAboutItemOrdering && $items->count() > 0) { // skip for now, move on to the next item
131 81
                $this->logger->debug("doesn't fit, skipping for now");
132
                $skippedItems[] = $itemToPack;
133 81
                // 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 39
                while ($items->count() > 1 && self::isSameDimensions($itemToPack, $items->top())) {
135
                    $skippedItems[] = $items->extract();
136 81
                }
137
                continue;
138
            }
139 94
140 89
            if ($x > $startX) {
141 89
                $this->logger->debug('No more fit in width wise, resetting for new row');
142 89
                $y += $rowLength;
143 89
                $x = $startX;
144 89
                $rowLength = 0;
145 89
                $skippedItems[] = $itemToPack;
146 89
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
147 89
                $skippedItems = [];
148 89
                $prevItem = null;
149
                continue;
150
            }
151 94
152 94
            $this->logger->debug('no items fit, so starting next vertical layer');
153
            $skippedItems[] = $itemToPack;
154 94
155
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
156 94
157
            return $layer;
158
        }
159 98
160
        return $layer;
161
    }
162
163
    /**
164
     * Compare two items to see if they have same dimensions.
165 55
     */
166
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
167 55
    {
168 34
        if ($itemA === $itemB) {
169
            return true;
170 33
        }
171 33
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
172 33
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
173 33
        sort($itemADimensions);
174
        sort($itemBDimensions);
175 33
176
        return $itemADimensions === $itemBDimensions;
177
    }
178
}
179