Passed
Push — master ( e748d4...4fe23d )
by Doug
02:40
created

LayerPacker::checkNonDimensionalConstraints()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 8
ccs 0
cts 5
cp 0
rs 10
cc 4
nc 4
nop 3
crap 20
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 70
    public function __construct(Box $box)
57
    {
58 70
        $this->box = $box;
59 70
        $this->logger = new NullLogger();
60
61 70
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
62 70
        $this->orientatedItemFactory->setLogger($this->logger);
63 70
    }
64
65
    /**
66
     * Sets a logger.
67
     */
68 70
    public function setLogger(LoggerInterface $logger): void
69
    {
70 70
        $this->logger = $logger;
71 70
        $this->orientatedItemFactory->setLogger($logger);
72 70
    }
73
74 58
    public function setSinglePassMode(bool $singlePassMode): void
75
    {
76 58
        $this->singlePassMode = $singlePassMode;
77 58
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
78 58
    }
79
80
    /**
81
     * Pack items into an individual vertical layer.
82
     */
83 70
    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 70
        $layer = new PackedLayer();
86 70
        $x = $startX;
87 70
        $y = $startY;
88 70
        $z = $startZ;
89 70
        $rowLength = 0;
90 70
        $prevItem = null;
91 70
        $skippedItems = [];
92 70
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
93
94 70
        while ($items->count() > 0) {
95 70
            $itemToPack = $items->extract();
96
97
            //skip items that will never fit e.g. too heavy
98 70
            if ($itemToPack->getWeight() > $remainingWeightAllowed) {
99 8
                continue;
100
            }
101
102 70
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
103
104 70
            if ($orientatedItem instanceof OrientatedItem) {
105 70
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
106 70
                $layer->insert($packedItem);
107 70
                $packedItemList->insert($packedItem);
108
109 70
                $rowLength = max($rowLength, $packedItem->getLength());
110 70
                $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 70
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
115 70
                if ($stackableDepth > 0) {
116 32
                    $stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->getDepth(), $x + $packedItem->getWidth(), $y + $packedItem->getLength(), $stackableDepth, $stackableDepth, $considerStability);
117 32
                    $layer->merge($stackedLayer);
118
                }
119
120
                //Having now placed an item, there is space *within the same row* along the length. Pack into that.
121 70
                if ($rowLength - $orientatedItem->getLength() > 0) {
122 20
                    $layer->merge($this->packLayer($items, $packedItemList, $x, $y + $orientatedItem->getLength(), $z, $widthForLayer, $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));
123
                }
124
125 70
                $x += $packedItem->getWidth();
126 70
                $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items
127
128 70
                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...
129 12
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
130 12
                    $skippedItems = [];
131
                }
132 70
                continue;
133
            }
134
135 62
            if ($items->count() > 0) { // skip for now, move on to the next item
136 52
                $this->logger->debug("doesn't fit, skipping for now");
137 52
                $skippedItems[] = $itemToPack;
138
                // 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
139 52
                while ($items->count() > 1 && static::isSameDimensions($itemToPack, $items->top())) {
140 38
                    $skippedItems[] = $items->extract();
141
                }
142 52
                continue;
143
            }
144
145 62
            if ($x > $startX) {
146 62
                $this->logger->debug('No more fit in width wise, resetting for new row');
147 62
                $y += $rowLength;
148 62
                $x = $startX;
149 62
                $rowLength = 0;
150 62
                $skippedItems[] = $itemToPack;
151 62
                $items = ItemList::fromArray($skippedItems, true);
152 62
                $skippedItems = [];
153 62
                $prevItem = null;
154 62
                continue;
155
            }
156
157 56
            $this->logger->debug('no items fit, so starting next vertical layer');
158 56
            $skippedItems[] = $itemToPack;
159
160 56
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
161
162 56
            return $layer;
163
        }
164
165 70
        return $layer;
166
    }
167
168
    /**
169
     * Compare two items to see if they have same dimensions.
170
     */
171 44
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
172
    {
173 44
        if ($itemA === $itemB) {
174 34
            return true;
175
        }
176 20
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
177 20
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
178 20
        sort($itemADimensions);
179 20
        sort($itemBDimensions);
180
181 20
        return $itemADimensions === $itemBDimensions;
182
    }
183
}
184