Passed
Push — 2.x-dev ( eccc16...ab1c97 )
by Doug
07:42
created

LayerPacker::checkNonDimensionalConstraints()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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