Passed
Push — master ( 8de8ac...4e9f9e )
by Doug
05:17 queued 02:42
created

LayerPacker::packLayer()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 82
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 53
CRAP Score 13

Importance

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

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