Completed
Push — refactor_layerpacker ( dfc43c )
by Doug
09:17
created

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