Passed
Push — 3.x ( 3ac6d2...8ba8a4 )
by Doug
02:05
created

LayerPacker::setSinglePassMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
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 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 54
    public function __construct(Box $box)
57
    {
58 54
        $this->box = $box;
59 54
        $this->logger = new NullLogger();
60
61 54
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
62 54
        $this->orientatedItemFactory->setLogger($this->logger);
63 54
    }
64
65
    /**
66
     * Sets a logger.
67
     */
68 54
    public function setLogger(LoggerInterface $logger): void
69
    {
70 54
        $this->logger = $logger;
71 54
        $this->orientatedItemFactory->setLogger($logger);
72 54
    }
73
74 49
    public function setSinglePassMode(bool $singlePassMode): void
75
    {
76 49
        $this->singlePassMode = $singlePassMode;
77 49
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
78 49
    }
79
80
    /**
81
     * Pack items into an individual vertical layer.
82
     */
83 54
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, array $layers, int $z, int $layerWidth, int $lengthLeft, int $depthLeft, int $guidelineLayerDepth, bool $considerStability): PackedLayer
84
    {
85 54
        $layer = new PackedLayer();
86 54
        $prevItem = null;
87 54
        $x = $y = $rowLength = 0;
88 54
        $skippedItems = [];
89 54
        $remainingWeightAllowed = $this->getRemainingWeightAllowed($layers);
90
91 54
        while ($items->count() > 0) {
92 54
            $itemToPack = $items->extract();
93
94
            //skip items that will never fit e.g. too heavy
95 54
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
96 7
                continue;
97
            }
98
99 54
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $layerWidth - $x, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
100
101 54
            if ($orientatedItem instanceof OrientatedItem) {
102 54
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
103 54
                $layer->insert($packedItem);
104 54
                $remainingWeightAllowed -= $itemToPack->getWeight();
105 54
                $packedItemList->insert($packedItem);
106
107 54
                $rowLength = max($rowLength, $packedItem->getLength());
108
109
                //Figure out if we can stack the next item vertically on top of this rather than side by side
110
                //e.g. when we've packed a tall item, and have just put a shorter one next to it.
111 54
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z, $considerStability);
112 54
                $x += $packedItem->getWidth();
113
114 54
                $prevItem = $orientatedItem;
115 54
                if ($items->count() === 0) {
116 51
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
117 51
                    $skippedItems = [];
118
                }
119 54
                continue;
120
            }
121
122 48
            if ($items->count() > 0) { // skip for now, move on to the next item
123 39
                $this->logger->debug("doesn't fit, skipping for now");
124 39
                $skippedItems[] = $itemToPack;
125
                // 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
126 39
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
127 21
                    $skippedItems[] = $items->extract();
128
                }
129 39
                continue;
130
            }
131
132 48
            if ($x > 0) {
133 47
                $this->logger->debug('No more fit in width wise, resetting for new row');
134 47
                $lengthLeft -= $rowLength;
135 47
                $y += $rowLength;
136 47
                $x = $rowLength = 0;
137 47
                $skippedItems[] = $itemToPack;
138 47
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
139 47
                $skippedItems = [];
140 47
                $prevItem = null;
141 47
                continue;
142
            }
143
144 43
            $this->logger->debug('no items fit, so starting next vertical layer');
145 43
            $skippedItems[] = $itemToPack;
146
147 43
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
148
149 43
            return $layer;
150
        }
151
152 53
        return $layer;
153
    }
154
155 54
    private function packVerticallyInsideItemFootprint(PackedLayer $layer, PackedItem $packedItem, PackedItemList $packedItemList, ItemList &$items, int &$remainingWeightAllowed, int $guidelineLayerDepth, int $rowLength, int $x, int $y, int $z, bool $considerStability): void
156
    {
157 54
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
158 54
        $stackedZ = $z + $packedItem->getDepth();
159 54
        $stackSkippedItems = [];
160 54
        $stackedItem = $packedItem->toOrientatedItem();
161 54
        while ($stackableDepth > 0 && $items->count() > 0) {
162 17
            $itemToTryStacking = $items->extract();
163
164
            //skip items that will never fit
165 17
            if (!$this->checkNonDimensionalConstraints($itemToTryStacking, $remainingWeightAllowed, $packedItemList)) {
166
                continue;
167
            }
168
169 17
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList, $considerStability);
170 17
            if ($stackedItem) {
171 11
                $packedStackedItem = PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ);
172 11
                $layer->insert($packedStackedItem);
173 11
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
174 11
                $packedItemList->insert($packedStackedItem);
175 11
                $stackableDepth -= $stackedItem->getDepth();
176 11
                $stackedZ += $stackedItem->getDepth();
177 11
                continue;
178
            }
179
180 13
            $stackSkippedItems[] = $itemToTryStacking;
181
            // abandon here if next item is the same, no point trying to keep going
182 13
            while ($items->count() > 0 && static::isSameDimensions($itemToTryStacking, $items->top())) {
183 8
                $stackSkippedItems[] = $items->extract();
184
            }
185
        }
186 54
        $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
187 54
    }
188
189
    /**
190
     * As well as purely dimensional constraints, there are other constraints that need to be met
191
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
192
     */
193 54
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
194
    {
195 54
        $customConstraintsOK = true;
196 54
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
197 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
198
        }
199
200 54
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
201
    }
202
203 54
    private function getRemainingWeightAllowed(array $layers): int
204
    {
205 54
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
206 54
        foreach ($layers as $layer) {
207 54
            $remainingWeightAllowed -= $layer->getWeight();
208
        }
209
210 54
        return $remainingWeightAllowed;
211
    }
212
213
    /**
214
     * Compare two items to see if they have same dimensions.
215
     */
216 28
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
217
    {
218 28
        if ($itemA === $itemB) {
219 19
            return true;
220
        }
221 13
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
222 13
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
223 13
        sort($itemADimensions);
224 13
        sort($itemBDimensions);
225
226 13
        return $itemADimensions === $itemBDimensions;
227
    }
228
}
229