Test Failed
Push — master ( 09012b...794d70 )
by Doug
04:43
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 max;
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\NullLogger;
15
16
/**
17
 * Layer packer.
18
 *
19
 * @internal
20
 * @author Doug Wright
21
 */
22
class LayerPacker implements LoggerAwareInterface
23
{
24
    /**
25
     * The logger instance.
26
     *
27
     * @var LoggerInterface
28
     */
29
    private $logger;
30
31
    /**
32
     * Box to pack items into.
33
     *
34
     * @var Box
35
     */
36
    private $box;
37
38
    /**
39
     * Whether the packer is in single-pass mode.
40
     *
41
     * @var bool
42
     */
43
    private $singlePassMode = false;
44
45
    /**
46
     * @var OrientatedItemFactory
47
     */
48
    private $orientatedItemFactory;
49
50
    /**
51
     * Constructor.
52
     */
53 41
    public function __construct(Box $box)
54
    {
55 41
        $this->box = $box;
56 41
        $this->logger = new NullLogger();
57
58 41
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
59 41
        $this->orientatedItemFactory->setLogger($this->logger);
60 41
    }
61
62
    /**
63
     * Sets a logger.
64
     */
65 41
    public function setLogger(LoggerInterface $logger): void
66
    {
67 41
        $this->logger = $logger;
68 41
        $this->orientatedItemFactory->setLogger($logger);
69 41
    }
70
71 23
    public function setSinglePassMode(bool $singlePassMode): void
72
    {
73 23
        $this->singlePassMode = $singlePassMode;
74 23
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
75 23
    }
76
77
    /**
78
     * Pack items into an individual vertical layer.
79
     */
80 41
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, array $layers, int $z, int $layerWidth, int $lengthLeft, int $depthLeft, int $guidelineLayerDepth): PackedLayer
81
    {
82 41
        $layer = new PackedLayer();
83 41
        $prevItem = null;
84 41
        $x = $y = $rowLength = 0;
85 41
        $skippedItems = [];
86 41
        $remainingWeightAllowed = $this->getRemainingWeightAllowed($layers);
87
88 41
        while ($items->count() > 0) {
89 41
            $itemToPack = $items->extract();
90
91
            //skip items that will never fit e.g. too heavy
92 41
            if ($itemToPack->getWeight() > $remainingWeightAllowed) {
93 4
                continue;
94
            }
95
96 41
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $layerWidth - $x, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, $packedItemList);
97
98 41
            if ($orientatedItem instanceof OrientatedItem) {
99 41
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
100 41
                $layer->insert($packedItem);
101 41
                $remainingWeightAllowed -= $itemToPack->getWeight();
102 41
                $packedItemList->insert($packedItem);
103
104 41
                $rowLength = max($rowLength, $packedItem->getLength());
105
106
                //Figure out if we can stack the next item vertically on top of this rather than side by side
107
                //e.g. when we've packed a tall item, and have just put a shorter one next to it.
108 41
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z);
109 41
                $x += $packedItem->getWidth();
110
111 41
                $prevItem = $orientatedItem;
112 41
                if ($items->count() === 0) {
113 37
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
114 37
                    $skippedItems = [];
115
                }
116 41
                continue;
117
            }
118
119 38
            if ($items->count() > 0) { // skip for now, move on to the next item
120 30
                $this->logger->debug("doesn't fit, skipping for now");
121 30
                $skippedItems[] = $itemToPack;
122
                // 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
123 30
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
124 8
                    $skippedItems[] = $items->extract();
125
                }
126 30
                continue;
127
            }
128
129 38
            if ($x > 0) {
130 37
                $this->logger->debug('No more fit in width wise, resetting for new row');
131 37
                $lengthLeft -= $rowLength;
132 37
                $y += $rowLength;
133 37
                $x = $rowLength = 0;
134 37
                $skippedItems[] = $itemToPack;
135 37
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
136 37
                $skippedItems = [];
137 37
                $prevItem = null;
138 37
                continue;
139
            }
140
141 30
            $this->logger->debug('no items fit, so starting next vertical layer');
142 30
            $skippedItems[] = $itemToPack;
143
144 30
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
145
146 30
            return $layer;
147
        }
148
149 39
        return $layer;
150
    }
151
152 41
    private function packVerticallyInsideItemFootprint(PackedLayer $layer, PackedItem $packedItem, PackedItemList $packedItemList, ItemList &$items, int &$remainingWeightAllowed, int $guidelineLayerDepth, int $rowLength, int $x, int $y, int $z): void
153
    {
154 41
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
155 41
        $stackedZ = $z + $packedItem->getDepth();
156 41
        $stackSkippedItems = [];
157 41
        $stackedItem = $packedItem->toOrientatedItem();
158 41
        while ($stackableDepth > 0 && $items->count() > 0) {
159 6
            $itemToTryStacking = $items->extract();
160
161
            //skip items that will never fit
162 6
            if ($itemToTryStacking->getWeight() > $remainingWeightAllowed) {
163
                continue;
164
            }
165
166 6
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList);
167 6
            if ($stackedItem) {
168 4
                $packedStackedItem = PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ);
169 4
                $layer->insert($packedStackedItem);
170 4
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
171 4
                $packedItemList->insert($packedStackedItem);
172 4
                $stackableDepth -= $stackedItem->getDepth();
173 4
                $stackedZ += $stackedItem->getDepth();
174 4
                continue;
175
            }
176
177 2
            $stackSkippedItems[] = $itemToTryStacking;
178
            // abandon here if next item is the same, no point trying to keep going
179 2
            while ($items->count() > 0 && static::isSameDimensions($itemToTryStacking, $items->top())) {
180
                $stackSkippedItems[] = $items->extract();
181
            }
182
        }
183 41
        $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
184 41
    }
185
186 41
    private function getRemainingWeightAllowed(array $layers): int
187
    {
188 41
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
189 41
        foreach ($layers as $layer) {
190 29
            $remainingWeightAllowed -= $layer->getWeight();
191
        }
192
193 41
        return $remainingWeightAllowed;
194
    }
195
196
    /**
197
     * Compare two items to see if they have same dimensions.
198
     */
199 12
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
200
    {
201 12
        if ($itemA === $itemB) {
202 8
            return true;
203
        }
204 4
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
205 4
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
206 4
        sort($itemADimensions);
207 4
        sort($itemBDimensions);
208
209 4
        return $itemADimensions === $itemBDimensions;
210
    }
211
}
212