Passed
Push — master ( 8de8ac...4e9f9e )
by Doug
05:17 queued 02:42
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 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
 * @internal
22
 */
23
class LayerPacker implements LoggerAwareInterface
24
{
25
    private LoggerInterface $logger;
26
27
    private Box $box;
28
29
    private bool $singlePassMode = false;
30
31
    private OrientatedItemFactory $orientatedItemFactory;
32
33
    private bool $beStrictAboutItemOrdering = false;
34
35 92
    public function __construct(Box $box)
36
    {
37 92
        $this->box = $box;
38 92
        $this->logger = new NullLogger();
39
40 92
        $this->orientatedItemFactory = new OrientatedItemFactory($this->box);
41 92
        $this->orientatedItemFactory->setLogger($this->logger);
42
    }
43
44
    /**
45
     * Sets a logger.
46
     */
47 92
    public function setLogger(LoggerInterface $logger): void
48
    {
49 92
        $this->logger = $logger;
50 92
        $this->orientatedItemFactory->setLogger($logger);
51
    }
52
53 22
    public function setSinglePassMode(bool $singlePassMode): void
54
    {
55 22
        $this->singlePassMode = $singlePassMode;
56 22
        $this->orientatedItemFactory->setSinglePassMode($singlePassMode);
57
    }
58
59 39
    public function beStrictAboutItemOrdering(bool $beStrict): void
60
    {
61 39
        $this->beStrictAboutItemOrdering = $beStrict;
62
    }
63
64
    /**
65
     * Pack items into an individual vertical layer.
66
     */
67 92
    public function packLayer(ItemList &$items, PackedItemList $packedItemList, int $startX, int $startY, int $startZ, int $widthForLayer, int $lengthForLayer, int $depthForLayer, int $guidelineLayerDepth, bool $considerStability): PackedLayer
68
    {
69 92
        $layer = new PackedLayer();
70 92
        $x = $startX;
71 92
        $y = $startY;
72 92
        $z = $startZ;
73 92
        $rowLength = 0;
74 92
        $prevItem = null;
75 92
        $skippedItems = [];
76 92
        $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight();
77
78 92
        while ($items->count() > 0) {
79 92
            $itemToPack = $items->extract();
80
81
            // skip items that will never fit e.g. too heavy
82 92
            if ($itemToPack->getWeight() > $remainingWeightAllowed) {
83 18
                continue;
84
            }
85
86 90
            $orientatedItem = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevItem, $items, $widthForLayer - $x, $lengthForLayer - $y, $depthForLayer, $rowLength, $x, $y, $z, $packedItemList, $considerStability);
87
88 90
            if ($orientatedItem instanceof OrientatedItem) {
89 89
                $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $z);
90 89
                $layer->insert($packedItem);
91 89
                $packedItemList->insert($packedItem);
92
93 89
                $rowLength = max($rowLength, $packedItem->getLength());
94 89
                $prevItem = $orientatedItem;
95
96
                // Figure out if we can stack items 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 89
                $stackableDepth = ($guidelineLayerDepth ?: $layer->getDepth()) - $packedItem->getDepth();
99 89
                if ($stackableDepth > 0) {
100 30
                    $stackedLayer = $this->packLayer($items, $packedItemList, $x, $y, $z + $packedItem->getDepth(), $x + $packedItem->getWidth(), $y + $packedItem->getLength(), $stackableDepth, $stackableDepth, $considerStability);
101 30
                    $layer->merge($stackedLayer);
102
                }
103
104 89
                $x += $packedItem->getWidth();
105 89
                $remainingWeightAllowed = $this->box->getMaxWeight() - $this->box->getEmptyWeight() - $packedItemList->getWeight(); // remember may have packed additional items
106
107 89
                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...
108 16
                    $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
109 16
                    $skippedItems = [];
110
                }
111 89
                continue;
112
            }
113
114 81
            if (!$this->beStrictAboutItemOrdering && $items->count() > 0) { // skip for now, move on to the next item
115 70
                $this->logger->debug("doesn't fit, skipping for now");
116 70
                $skippedItems[] = $itemToPack;
117
                // 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
118 70
                while ($items->count() > 1 && self::isSameDimensions($itemToPack, $items->top())) {
119 32
                    $skippedItems[] = $items->extract();
120
                }
121 70
                continue;
122
            }
123
124 81
            if ($x > $startX) {
125
                // Having now placed items, there is space *within the same row* along the length. Pack into that.
126 80
                $this->logger->debug('No more fit in width wise, packing along remaining length');
127 80
                $layer->merge($this->packLayer($items, $packedItemList, $x, $y + $rowLength, $z, $widthForLayer, $lengthForLayer - $rowLength, $depthForLayer, $layer->getDepth(), $considerStability));
128
129 80
                $this->logger->debug('No more fit in width wise, resetting for new row');
130 80
                $y += $rowLength;
131 80
                $x = $startX;
132 80
                $rowLength = 0;
133 80
                $skippedItems[] = $itemToPack;
134 80
                $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
135 80
                $skippedItems = [];
136 80
                $prevItem = null;
137 80
                continue;
138
            }
139
140 71
            $this->logger->debug('no items fit, so starting next vertical layer');
141 71
            $skippedItems[] = $itemToPack;
142
143 71
            $items = ItemList::fromArray(array_merge($skippedItems, iterator_to_array($items)), true);
144
145 71
            return $layer;
146
        }
147
148 91
        return $layer;
149
    }
150
151
    /**
152
     * Compare two items to see if they have same dimensions.
153
     */
154 42
    private static function isSameDimensions(Item $itemA, Item $itemB): bool
155
    {
156 42
        if ($itemA === $itemB) {
157 29
            return true;
158
        }
159 21
        $itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
160 21
        $itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
161 21
        sort($itemADimensions);
162 21
        sort($itemBDimensions);
163
164 21
        return $itemADimensions === $itemBDimensions;
165
    }
166
}
167