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