Test Failed
Push — 2.x-dev ( 5e90af...eccc16 )
by Doug
03:16
created

LayerPacker::getRemainingWeightAllowed()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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