Passed
Push — master ( 73ddcb...629397 )
by Doug
02:34
created

LayerPacker   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 188
Duplicated Lines 0 %

Test Coverage

Coverage 98.95%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 24
eloc 88
c 7
b 0
f 0
dl 0
loc 188
ccs 94
cts 95
cp 0.9895
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getRemainingWeightAllowed() 0 8 2
A isSameDimensions() 0 11 2
A __construct() 0 7 1
B packLayer() 0 70 9
A setLogger() 0 4 1
A setSinglePassMode() 0 4 1
B packVerticallyInsideItemFootprint() 0 32 8
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 73
    public function __construct(Box $box)
54
    {
55 73
        $this->box = $box;
56 73
        $this->logger = new NullLogger();
57
58 73
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
59 73
        $this->orientatedItemFactory->setLogger($this->logger);
60 73
    }
61
62
    /**
63
     * Sets a logger.
64
     */
65 73
    public function setLogger(LoggerInterface $logger): void
66
    {
67 73
        $this->logger = $logger;
68 73
        $this->orientatedItemFactory->setLogger($logger);
69 73
    }
70
71 53
    public function setSinglePassMode(bool $singlePassMode): void
72
    {
73 53
        $this->singlePassMode = $singlePassMode;
74 53
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
75 53
    }
76
77
    /**
78
     * Pack items into an individual vertical layer.
79
     */
80 73
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, array $layers, int $z, int $layerWidth, int $lengthLeft, int $depthLeft, int $guidelineLayerDepth): PackedLayer
81
    {
82 73
        $layer = new PackedLayer();
83 73
        $prevItem = null;
84 73
        $x = $y = $rowLength = 0;
85 73
        $skippedItems = [];
86 73
        $remainingWeightAllowed = $this->getRemainingWeightAllowed($layers);
87
88 73
        while ($items->count() > 0) {
89 73
            $itemToPack = $items->extract();
90
91
            //skip items that will never fit e.g. too heavy
92 73
            if ($itemToPack->getWeight() > $remainingWeightAllowed) {
93 8
                continue;
94
            }
95
96 73
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $layerWidth - $x, $lengthLeft, $depthLeft, $rowLength, $x, $y, $z, $packedItemList);
97
98 73
            if ($orientatedItem instanceof OrientatedItem) {
99 73
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
100 73
                $layer->insert($packedItem);
101 73
                $remainingWeightAllowed -= $itemToPack->getWeight();
102 73
                $packedItemList->insert($packedItem);
103
104 73
                $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 73
                $this->packVerticallyInsideItemFootprint($layer, $packedItem, $packedItemList, $items, $remainingWeightAllowed, $guidelineLayerDepth, $rowLength, $x, $y, $z);
109 73
                $x += $packedItem->getWidth();
110
111 73
                $prevItem = $orientatedItem;
112 73
                if ($items->count() === 0) {
113 68
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
114 68
                    $skippedItems = [];
115
                }
116 73
                continue;
117
            }
118
119 66
            if ($items->count() > 0) { // skip for now, move on to the next item
120 53
                $this->logger->debug("doesn't fit, skipping for now");
121 53
                $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 53
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
124 24
                    $skippedItems[] = $items->extract();
125
                }
126 53
                continue;
127
            }
128
129 66
            if ($x > 0) {
130 65
                $this->logger->debug('No more fit in width wise, resetting for new row');
131 65
                $lengthLeft -= $rowLength;
132 65
                $y += $rowLength;
133 65
                $x = $rowLength = 0;
134 65
                $skippedItems[] = $itemToPack;
135 65
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
136 65
                $skippedItems = [];
137 65
                $prevItem = null;
138 65
                continue;
139
            }
140
141 55
            $this->logger->debug('no items fit, so starting next vertical layer');
142 55
            $skippedItems[] = $itemToPack;
143
144 55
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
145
146 55
            return $layer;
147
        }
148
149 71
        return $layer;
150
    }
151
152 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
153
    {
154 73
        $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
155 73
        $stackedZ = $z + $packedItem->getDepth();
156 73
        $stackSkippedItems = [];
157 73
        $stackedItem = $packedItem->toOrientatedItem();
158 73
        while ($stackableDepth > 0 && $items->count() > 0) {
159 19
            $itemToTryStacking = $items->extract();
160
161
            //skip items that will never fit
162 19
            if ($itemToTryStacking->getWeight() > $remainingWeightAllowed) {
163
                continue;
164
            }
165
166 19
            $stackedItem = $this->orientatedItemFactory->getBestOrientation($itemToTryStacking, $stackedItem, $items, $packedItem->getWidth(), $packedItem->getLength(), $stackableDepth, $rowLength, $x, $y, $stackedZ, $packedItemList);
167 19
            if ($stackedItem) {
168 13
                $packedStackedItem = PackedItem::fromOrientatedItem($stackedItem, $x, $y, $stackedZ);
169 13
                $layer->insert($packedStackedItem);
170 13
                $remainingWeightAllowed -= $itemToTryStacking->getWeight();
171 13
                $packedItemList->insert($packedStackedItem);
172 13
                $stackableDepth -= $stackedItem->getDepth();
173 13
                $stackedZ += $stackedItem->getDepth();
174 13
                continue;
175
            }
176
177 13
            $stackSkippedItems[] = $itemToTryStacking;
178
            // abandon here if next item is the same, no point trying to keep going
179 13
            while ($items->count() > 0 && static::isSameDimensions($itemToTryStacking, $items->top())) {
180 8
                $stackSkippedItems[] = $items->extract();
181
            }
182
        }
183 73
        $items = ItemList::fromArray(array_merge($stackSkippedItems, iterator_to_array($items)), true);
184 73
    }
185
186 73
    private function getRemainingWeightAllowed(array $layers): int
187
    {
188 73
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
189 73
        foreach ($layers as $layer) {
190 54
            $remainingWeightAllowed -= $layer->getWeight();
191
        }
192
193 73
        return $remainingWeightAllowed;
194
    }
195
196
    /**
197
     * Compare two items to see if they have same dimensions.
198
     */
199 33
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
200
    {
201 33
        if ($itemA === $itemB) {
202 22
            return true;
203
        }
204 15
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
205 15
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
206 15
        sort($itemADimensions);
207 15
        sort($itemBDimensions);
208
209 15
        return $itemADimensions === $itemBDimensions;
210
    }
211
}
212