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