Passed
Push — 3.x ( 97397b...854a27 )
by Doug
12:23 queued 10:33
created

LayerPacker::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 1
rs 10
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 Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerInterface;
13
use Psr\Log\NullLogger;
14
15
use function array_merge;
16
use function iterator_to_array;
17
use function max;
18
use function sort;
19
20
/**
21
 * Layer packer.
22
 *
23
 * @internal
24
 * @author Doug Wright
25
 */
26
class LayerPacker implements LoggerAwareInterface
27
{
28
    /**
29
     * The logger instance.
30
     */
31
    private LoggerInterface $logger;
32
33
    /**
34
     * Box to pack items into.
35
     */
36
    private Box $box;
37
38
    /**
39
     * Whether the packer is in single-pass mode.
40
     */
41
    private bool $singlePassMode = false;
42
43
    private OrientatedItemFactory $orientatedItemFactory;
44
45
    private bool $beStrictAboutItemOrdering = false;
46
47
    /**
48
     * Constructor.
49
     */
50 84
    public function __construct(Box $box)
51
    {
52 84
        $this->box = $box;
53 84
        $this->logger = new NullLogger();
54
55 84
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
56 84
        $this->orientatedItemFactory->setLogger($this->logger);
57
    }
58
59
    /**
60
     * Sets a logger.
61
     */
62 84
    public function setLogger(LoggerInterface $logger): void
63
    {
64 84
        $this->logger = $logger;
65 84
        $this->orientatedItemFactory->setLogger($logger);
66
    }
67
68 51
    public function setSinglePassMode(bool $singlePassMode): void
69
    {
70 51
        $this->singlePassMode = $singlePassMode;
71 51
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
72
    }
73
74 38
    public function beStrictAboutItemOrdering(bool $beStrict): void
75
    {
76 38
        $this->beStrictAboutItemOrdering = $beStrict;
77
    }
78
79
    /**
80
     * Pack items into an individual vertical layer.
81
     */
82 84
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability): PackedLayer
83
    {
84 84
        $layer = new PackedLayer();
85 84
        $x = $startX;
86 84
        $y = $startY;
87 84
        $z = $startZ;
88 84
        $rowLength = 0;
89 84
        $prevItem = null;
90 84
        $skippedItems = [];
91 84
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
92
93 84
        while ($items->count() > 0) {
94 84
            $itemToPack = $items->extract();
95
96
            // skip items that will never fit e.g. too heavy
97 84
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
98 11
                continue;
99
            }
100
101 83
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
102
103 83
            if ($orientatedItem instanceof OrientatedItem) {
104 83
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
105 83
                $layer->insert($packedItem);
106 83
                $packedItemList->insert($packedItem);
107
108 83
                $rowLength = max($rowLength, $packedItem->getLength());
109 83
                $prevItem = $orientatedItem;
110
111
                // Figure out if we can stack items on top of this rather than side by side
112
                // e.g. when we've packed a tall item, and have just put a shorter one next to it.
113 83
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
114 83
                if ($stackableDepth > 0) {
115 29
                    $stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->getDepth(), $x + $packedItem->getWidth(), $y + $packedItem->getLength(), $stackableDepth, $stackableDepth, $considerStability);
116 29
                    $layer->merge($stackedLayer);
117
                }
118
119 83
                $x += $packedItem->getWidth();
120 83
                $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items
121
122 83
                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 11
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
124 11
                    $skippedItems = [];
125
                }
126 83
                continue;
127
            }
128
129 74
            if (!$this->beStrictAboutItemOrdering && $items->count() > 0) { // skip for now, move on to the next item
130 62
                $this->logger->debug("doesn't fit, skipping for now");
131 62
                $skippedItems[] = $itemToPack;
132
                // 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
133 62
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
134 30
                    $skippedItems[] = $items->extract();
135
                }
136 62
                continue;
137
            }
138
139 74
            if ($x > $startX) {
140
                // Having now placed items, there is space *within the same row* along the length. Pack into that.
141 74
                $this->logger->debug('No more fit in width wise, packing along remaining length');
142 74
                $layer->merge($this->packLayer($items, $packedItemList, $x, $y + $rowLength, $z, $widthForLayer, $lengthForLayer - $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));
143
144 74
                $this->logger->debug('No more fit in width wise, resetting for new row');
145 74
                $y += $rowLength;
146 74
                $x = $startX;
147 74
                $rowLength = 0;
148 74
                $skippedItems[] = $itemToPack;
149 74
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
150 74
                $skippedItems = [];
151 74
                $prevItem = null;
152 74
                continue;
153
            }
154
155 64
            $this->logger->debug('no items fit, so starting next vertical layer');
156 64
            $skippedItems[] = $itemToPack;
157
158 64
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
159
160 64
            return $layer;
161
        }
162
163 84
        return $layer;
164
    }
165
166
    /**
167
     * As well as purely dimensional constraints, there are other constraints that need to be met
168
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
169
     */
170 84
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
171
    {
172 84
        $customConstraintsOK = true;
173 84
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
174 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
175
        }
176
177 84
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
178
    }
179
180
    /**
181
     * Compare two items to see if they have same dimensions.
182
     */
183 36
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
184
    {
185 36
        if ($itemA === $itemB) {
186 27
            return true;
187
        }
188 16
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
189 16
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
190 16
        sort($itemADimensions);
191 16
        sort($itemBDimensions);
192
193 16
        return $itemADimensions === $itemBDimensions;
194
    }
195
}
196