Passed
Push — 3.x ( 922c95...5b6501 )
by Doug
12:25 queued 11:05
created

LayerPacker::checkNonDimensionalConstraints()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 4
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 4
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 73
    public function __construct(Box $box)
57
    {
58 73
        $this->box = $box;
59 73
        $this->logger = new NullLogger();
60
61 73
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
62 73
        $this->orientatedItemFactory->setLogger($this->logger);
63 73
    }
64
65
    /**
66
     * Sets a logger.
67
     */
68 73
    public function setLogger(LoggerInterface $logger): void
69
    {
70 73
        $this->logger = $logger;
71 73
        $this->orientatedItemFactory->setLogger($logger);
72 73
    }
73
74 53
    public function setSinglePassMode(bool $singlePassMode): void
75
    {
76 53
        $this->singlePassMode = $singlePassMode;
77 53
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
78 53
    }
79
80
    /**
81
     * Pack items into an individual vertical layer.
82
     */
83 73
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, array $layers, int $z, int $layerWidth, int $lengthLeft, int $depthLeft, int $guidelineLayerDepth): PackedLayer
84
    {
85 73
        $layer = new PackedLayer();
86 73
        $prevItem = null;
87 73
        $x = $y = $rowLength = 0;
88 73
        $skippedItems = [];
89 73
        $remainingWeightAllowed = $this->getRemainingWeightAllowed($layers);
90
91 73
        while ($items->count() > 0) {
92 73
            $itemToPack = $items->extract();
93
94
            //skip items that will never fit e.g. too heavy
95 73
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
96 9
                continue;
97
            }
98
99 73
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $layerWidth - $x, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, $packedItemList);
100
101 73
            if ($orientatedItem instanceof OrientatedItem) {
102 73
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
103 73
                $layer->insert($packedItem);
104 73
                $remainingWeightAllowed -= $itemToPack->getWeight();
105 73
                $packedItemList->insert($packedItem);
106
107 73
                $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 73
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z);
112 73
                $x += $packedItem->getWidth();
113
114 73
                $prevItem = $orientatedItem;
115 73
                if ($items->count() === 0) {
116 68
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
117 68
                    $skippedItems = [];
118
                }
119 73
                continue;
120
            }
121
122 65
            if ($items->count() > 0) { // skip for now, move on to the next item
123 53
                $this->logger->debug("doesn't fit, skipping for now");
124 53
                $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 53
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
127 24
                    $skippedItems[] = $items->extract();
128
                }
129 53
                continue;
130
            }
131
132 65
            if ($x > 0) {
133 64
                $this->logger->debug('No more fit in width wise, resetting for new row');
134 64
                $lengthLeft -= $rowLength;
135 64
                $y += $rowLength;
136 64
                $x = $rowLength = 0;
137 64
                $skippedItems[] = $itemToPack;
138 64
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
139 64
                $skippedItems = [];
140 64
                $prevItem = null;
141 64
                continue;
142
            }
143
144 54
            $this->logger->debug('no items fit, so starting next vertical layer');
145 54
            $skippedItems[] = $itemToPack;
146
147 54
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
148
149 54
            return $layer;
150
        }
151
152 71
        return $layer;
153
    }
154
155 73
    private function packVerticallyInsideItemFootprint(PackedLayer $layer, PackedItem $packedItem, PackedItemList $packedItemList, ItemList &$items, int &$remainingWeightAllowed, int $guidelineLayerDepth, int $rowLength, int $x, int $y, int $z): void
156
    {
157 73
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
158 73
        $stackedZ = $z + $packedItem->getDepth();
159 73
        $stackSkippedItems = [];
160 73
        $stackedItem = $packedItem->toOrientatedItem();
161 73
        while ($stackableDepth > 0 && $items->count() > 0) {
162 19
            $itemToTryStacking = $items->extract();
163
164
            //skip items that will never fit
165 19
            if (!$this->checkNonDimensionalConstraints($itemToTryStacking, $remainingWeightAllowed, $packedItemList)) {
166
                continue;
167
            }
168
169 19
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList);
170 19
            if ($stackedItem) {
171 13
                $packedStackedItem = PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ);
172 13
                $layer->insert($packedStackedItem);
173 13
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
174 13
                $packedItemList->insert($packedStackedItem);
175 13
                $stackableDepth -= $stackedItem->getDepth();
176 13
                $stackedZ += $stackedItem->getDepth();
177 13
                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 73
        $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
187 73
    }
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 73
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
194
    {
195 73
        $customConstraintsOK = true;
196 73
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
197 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
198
        }
199
200 73
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
201
    }
202
203 73
    private function getRemainingWeightAllowed(array $layers): int
204
    {
205 73
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
206 73
        foreach ($layers as $layer) {
207 53
            $remainingWeightAllowed -= $layer->getWeight();
208
        }
209
210 73
        return $remainingWeightAllowed;
211
    }
212
213
    /**
214
     * Compare two items to see if they have same dimensions.
215
     */
216 33
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
217
    {
218 33
        if ($itemA === $itemB) {
219 22
            return true;
220
        }
221 15
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
222 15
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
223 15
        sort($itemADimensions);
224 15
        sort($itemBDimensions);
225
226 15
        return $itemADimensions === $itemBDimensions;
227
    }
228
}
229