Passed
Push — master ( 723f6d...528355 )
by Doug
14:53
created

PackedBox::getItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
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 JsonSerializable;
12
13
use function iterator_to_array;
14
use function json_encode;
15
use function max;
16
use function round;
17
use function is_iterable;
18
use function count;
19
use function array_pop;
20
use function assert;
21
use function rawurlencode;
22
23
use const JSON_THROW_ON_ERROR;
24
use const JSON_NUMERIC_CHECK;
25
use const JSON_UNESCAPED_UNICODE;
26
use const JSON_UNESCAPED_SLASHES;
27
28
/**
29
 * A "box" with items.
30
 */
31
class PackedBox implements JsonSerializable
32
{
33
    protected readonly int $itemWeight;
34
35
    protected readonly float $volumeUtilisation;
36
37
    /**
38
     * Get packed weight.
39
     *
40 45
     * @return int weight in grams
41
     */
42 45
    public function getWeight(): int
43
    {
44
        return $this->box->getEmptyWeight() + $this->getItemWeight();
45
    }
46
47
    /**
48 98
     * Get packed weight of the items only.
49
     *
50 98
     * @return int weight in grams
51
     */
52
    public function getItemWeight(): int
53
    {
54
        if (!isset($this->itemWeight)) {
55
            $itemWeight = 0;
56
            foreach ($this->items as $item) {
57
                $itemWeight += $item->item->getWeight();
58 13
            }
59
            $this->itemWeight = $itemWeight;
0 ignored issues
show
Bug introduced by
The property itemWeight is declared read-only in DVDoug\BoxPacker\PackedBox.
Loading history...
60 13
        }
61
62
        return $this->itemWeight;
63
    }
64
65
    /**
66
     * Get remaining width inside box for another item.
67
     */
68 14
    public function getRemainingWidth(): int
69
    {
70 14
        return $this->box->getInnerWidth() - $this->getUsedWidth();
71
    }
72
73
    /**
74
     * Get remaining length inside box for another item.
75
     */
76 1
    public function getRemainingLength(): int
77
    {
78 1
        return $this->box->getInnerLength() - $this->getUsedLength();
79
    }
80
81
    /**
82
     * Get remaining depth inside box for another item.
83
     */
84 1
    public function getRemainingDepth(): int
85
    {
86 1
        return $this->box->getInnerDepth() - $this->getUsedDepth();
87
    }
88
89
    /**
90
     * Used width inside box for packing items.
91
     */
92 1
    public function getUsedWidth(): int
93
    {
94 1
        $maxWidth = 0;
95
96
        foreach ($this->items as $item) {
97
            $maxWidth = max($maxWidth, $item->x + $item->width);
98
        }
99
100 5
        return $maxWidth;
101
    }
102 5
103
    /**
104 5
     * Used length inside box for packing items.
105 5
     */
106
    public function getUsedLength(): int
107
    {
108 5
        $maxLength = 0;
109
110
        foreach ($this->items as $item) {
111
            $maxLength = max($maxLength, $item->y + $item->length);
112
        }
113
114 5
        return $maxLength;
115
    }
116 5
117
    /**
118 5
     * Used depth inside box for packing items.
119 5
     */
120
    public function getUsedDepth(): int
121
    {
122 5
        $maxDepth = 0;
123
124
        foreach ($this->items as $item) {
125
            $maxDepth = max($maxDepth, $item->z + $item->depth);
126
        }
127
128 5
        return $maxDepth;
129
    }
130 5
131
    /**
132 5
     * Get remaining weight inside box for another item.
133 5
     */
134
    public function getRemainingWeight(): int
135
    {
136 5
        return $this->box->getMaxWeight() - $this->getWeight();
137
    }
138
139
    public function getInnerVolume(): int
140
    {
141
        return $this->box->getInnerWidth() * $this->box->getInnerLength() * $this->box->getInnerDepth();
142 1
    }
143
144 1
    /**
145
     * Get used volume of the packed box.
146
     */
147 104
    public function getUsedVolume(): int
148
    {
149 104
        return $this->items->getVolume();
150
    }
151
152
    /**
153
     * Get unused volume of the packed box.
154
     */
155 104
    public function getUnusedVolume(): int
156
    {
157 104
        return $this->getInnerVolume() - $this->getUsedVolume();
158
    }
159
160
    /**
161
     * Get volume utilisation of the packed box.
162
     */
163 1
    public function getVolumeUtilisation(): float
164
    {
165 1
        if (!isset($this->volumeUtilisation)) {
166
            $this->volumeUtilisation = round($this->getUsedVolume() / ($this->getInnerVolume() ?: 1) * 100, 1);
0 ignored issues
show
Bug introduced by
The property volumeUtilisation is declared read-only in DVDoug\BoxPacker\PackedBox.
Loading history...
167
        }
168
169
        return $this->volumeUtilisation;
170
    }
171 30
172
    /**
173 30
     * Create a custom website visualiser URL for this packing.
174
     */
175
    public function generateVisualisationURL(): string
176
    {
177
        return 'https://boxpacker.io/en/master/visualiser.html?packing=' . rawurlencode(json_encode($this, flags: JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
178
    }
179 1
180
    public function __construct(public readonly Box $box, public readonly PackedItemList $items)
181 1
    {
182
        assert($this->assertPackingCompliesWithRealWorld());
183
    }
184 104
185
    public function jsonSerialize(): array
186 104
    {
187 101
        $userValues = [];
188
189 104
        if ($this->box instanceof JsonSerializable) {
190
            $userSerialisation = $this->box->jsonSerialize();
0 ignored issues
show
Bug introduced by
The method jsonSerialize() does not exist on DVDoug\BoxPacker\Box. It seems like you code against a sub-type of DVDoug\BoxPacker\Box such as DVDoug\BoxPacker\WorkingVolume or DVDoug\BoxPacker\Test\TestBox. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

190
            /** @scrutinizer ignore-call */ 
191
            $userSerialisation = $this->box->jsonSerialize();
Loading history...
191
            if (is_iterable($userSerialisation)) {
192
                $userValues = $userSerialisation;
193 4
            } else {
194
                $userValues = ['extra' => $userSerialisation];
195 4
            }
196
        }
197 4
198 3
        return [
199 3
            'box' => [
200 2
                ...$userValues,
201
                'reference' => $this->box->getReference(),
202 1
                'innerWidth' => $this->box->getInnerWidth(),
203
                'innerLength' => $this->box->getInnerLength(),
204
                'innerDepth' => $this->box->getInnerDepth(),
205
            ],
206 4
            'items' => iterator_to_array($this->items),
207 4
        ];
208 4
    }
209 4
210 4
    /**
211 4
     * Validate that all items are placed solely within the confines of the box, and that no two items are placed
212 4
     * into the same physical space.
213 4
     */
214 4
    private function assertPackingCompliesWithRealWorld(): bool
215 4
    {
216
        /** @var PackedItem[] $itemsToCheck */
217
        $itemsToCheck = iterator_to_array($this->items);
218
        while (count($itemsToCheck) > 0) {
219
            $itemToCheck = array_pop($itemsToCheck);
220
221
            assert($itemToCheck->x >= 0);
222
            assert($itemToCheck->x + $itemToCheck->width <= $this->box->getInnerWidth());
223
            assert($itemToCheck->y >= 0);
224
            assert($itemToCheck->y + $itemToCheck->length <= $this->box->getInnerLength());
225
            assert($itemToCheck->z >= 0);
226
            assert($itemToCheck->z + $itemToCheck->depth <= $this->box->getInnerDepth());
227
228
            foreach ($itemsToCheck as $otherItem) {
229
                $hasXOverlap = $itemToCheck->x < ($otherItem->x + $otherItem->width) && $otherItem->x < ($itemToCheck->x + $itemToCheck->width);
230
                $hasYOverlap = $itemToCheck->y < ($otherItem->y + $otherItem->length) && $otherItem->y < ($itemToCheck->y + $itemToCheck->length);
231
                $hasZOverlap = $itemToCheck->z < ($otherItem->z + $otherItem->depth) && $otherItem->z < ($itemToCheck->z + $itemToCheck->depth);
232
233
                $hasOverlap = $hasXOverlap && $hasYOverlap && $hasZOverlap;
234
                assert(!$hasOverlap);
235
            }
236
        }
237
238
        return true;
239
    }
240
}
241