Passed
Push — 3.x ( fece24...05a09a )
by Doug
02:30
created

LayerPacker::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 7
ccs 5
cts 5
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 array_merge;
12
use function iterator_to_array;
13
use function max;
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\NullLogger;
17
use function sort;
18
19
/**
20
 * Layer packer.
21
 *
22
 * @internal
23
 * @author Doug Wright
24
 */
25
class LayerPacker implements LoggerAwareInterface
26
{
27
    /**
28
     * The logger instance.
29
     *
30
     * @var LoggerInterface
31
     */
32
    private $logger;
33
34
    /**
35
     * Box to pack items into.
36
     *
37
     * @var Box
38
     */
39
    private $box;
40
41
    /**
42
     * Whether the packer is in single-pass mode.
43
     *
44
     * @var bool
45
     */
46
    private $singlePassMode = false;
47
48
    /**
49
     * @var OrientatedItemFactory
50
     */
51
    private $orientatedItemFactory;
52
53
    /**
54
     * Constructor.
55
     */
56 81
    public function __construct(Box $box)
57
    {
58 81
        $this->box = $box;
59 81
        $this->logger = new NullLogger();
60
61 81
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
62 81
        $this->orientatedItemFactory->setLogger($this->logger);
63 81
    }
64
65
    /**
66
     * Sets a logger.
67
     */
68 81
    public function setLogger(LoggerInterface $logger): void
69
    {
70 81
        $this->logger = $logger;
71 81
        $this->orientatedItemFactory->setLogger($logger);
72 81
    }
73
74 48
    public function setSinglePassMode(bool $singlePassMode): void
75
    {
76 48
        $this->singlePassMode = $singlePassMode;
77 48
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
78 48
    }
79
80
    /**
81
     * Pack items into an individual vertical layer.
82
     */
83 81
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability): PackedLayer
84
    {
85 81
        $layer = new PackedLayer();
86 81
        $x = $startX;
87 81
        $y = $startY;
88 81
        $z = $startZ;
89 81
        $rowLength = 0;
90 81
        $prevItem = null;
91 81
        $skippedItems = [];
92 81
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
93
94 81
        while ($items->count() > 0) {
95 81
            $itemToPack = $items->extract();
96
97
            //skip items that will never fit e.g. too heavy
98 81
            if (!$this->checkNonDimensionalConstraints($itemToPack, $remainingWeightAllowed, $packedItemList)) {
99 11
                continue;
100
            }
101
102 80
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
103
104 80
            if ($orientatedItem instanceof OrientatedItem) {
105 80
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
106 80
                $layer->insert($packedItem);
107 80
                $packedItemList->insert($packedItem);
108
109 80
                $rowLength = max($rowLength, $packedItem->getLength());
110 80
                $prevItem = $orientatedItem;
111
112
                //Figure out if we can stack items on top of this rather than side by side
113
                //e.g. when we've packed a tall item, and have just put a shorter one next to it.
114 80
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
115 80
                if ($stackableDepth > 0) {
116 27
                    $stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->getDepth(), $x + $packedItem->getWidth(), $y + $packedItem->getLength(), $stackableDepth, $stackableDepth, $considerStability);
117 27
                    $layer->merge($stackedLayer);
118
                }
119
120 80
                $x += $packedItem->getWidth();
121 80
                $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items
122
123 80
                if ($items->count() === 0 && $skippedItems) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $skippedItems of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
124 11
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
125 11
                    $skippedItems = [];
126
                }
127 80
                continue;
128
            }
129
130 71
            if ($items->count() > 0) { // skip for now, move on to the next item
131 61
                $this->logger->debug("doesn't fit, skipping for now");
132 61
                $skippedItems[] = $itemToPack;
133
                // 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
134 61
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
135 29
                    $skippedItems[] = $items->extract();
136
                }
137 61
                continue;
138
            }
139
140 71
            if ($x > $startX) {
141
                //Having now placed items, there is space *within the same row* along the length. Pack into that.
142 71
                $this->logger->debug('No more fit in width wise, packing along remaining length');
143 71
                $layer->merge($this->packLayer($items, $packedItemList, $x, $y + $rowLength, $z, $widthForLayer, $lengthForLayer - $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));
144
145 71
                $this->logger->debug('No more fit in width wise, resetting for new row');
146 71
                $y += $rowLength;
147 71
                $x = $startX;
148 71
                $rowLength = 0;
149 71
                $skippedItems[] = $itemToPack;
150 71
                $items = ItemList::fromArray($skippedItems, true);
151 71
                $skippedItems = [];
152 71
                $prevItem = null;
153 71
                continue;
154
            }
155
156 61
            $this->logger->debug('no items fit, so starting next vertical layer');
157 61
            $skippedItems[] = $itemToPack;
158
159 61
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
160
161 61
            return $layer;
162
        }
163
164 81
        return $layer;
165
    }
166
167
    /**
168
     * As well as purely dimensional constraints, there are other constraints that need to be met
169
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
170
     */
171 81
    private function checkNonDimensionalConstraints(Item $itemToPack, int $remainingWeightAllowed, PackedItemList $packedItemList): bool
172
    {
173 81
        $customConstraintsOK = true;
174 81
        if ($itemToPack instanceof ConstrainedItem && !$this->box instanceof WorkingVolume) {
175 1
            $customConstraintsOK = $itemToPack->canBePackedInBox($packedItemList, $this->box);
176
        }
177
178 81
        return $customConstraintsOK && $itemToPack->getWeight() <= $remainingWeightAllowed;
179
    }
180
181
    /**
182
     * Compare two items to see if they have same dimensions.
183
     */
184 35
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
185
    {
186 35
        if ($itemA === $itemB) {
187 26
            return true;
188
        }
189 15
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
190 15
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
191 15
        sort($itemADimensions);
192 15
        sort($itemBDimensions);
193
194 15
        return $itemADimensions === $itemBDimensions;
195
    }
196
}
197