Passed
Push — master ( 2e0db4...02b4d3 )
by Doug
02:32
created

PackedBox::assertPackingCompliesWithRealWorld()   B

Complexity

Conditions 8
Paths 26

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 15
nc 26
nop 0
dl 0
loc 21
ccs 9
cts 9
cp 1
crap 8
rs 8.4444
c 0
b 0
f 0
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 urlencode;
18
use function is_iterable;
19
use function count;
20
use function array_pop;
21
use function assert;
22
23
/**
24
 * A "box" with items.
25
 */
26
class PackedBox implements JsonSerializable
27
{
28
    protected int $itemWeight = 0;
29
30
    protected readonly float $volumeUtilisation;
31
32
    /**
33
     * Get box used.
34
     */
35 44
    public function getBox(): Box
36
    {
37 44
        return $this->box;
38
    }
39
40
    /**
41
     * Get items packed.
42
     */
43 93
    public function getItems(): PackedItemList
0 ignored issues
show
Bug introduced by
The type DVDoug\BoxPacker\PackedItemList was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
44
    {
45 93
        return $this->items;
46
    }
47
48
    /**
49
     * Get packed weight.
50
     *
51
     * @return int weight in grams
52
     */
53 12
    public function getWeight(): int
54
    {
55 12
        return $this->box->getEmptyWeight() + $this->getItemWeight();
56
    }
57
58
    /**
59
     * Get packed weight of the items only.
60
     *
61
     * @return int weight in grams
62
     */
63 13
    public function getItemWeight(): int
64
    {
65 13
        return $this->itemWeight;
66
    }
67
68
    /**
69
     * Get remaining width inside box for another item.
70
     */
71 1
    public function getRemainingWidth(): int
72
    {
73 1
        return $this->box->getInnerWidth() - $this->getUsedWidth();
74
    }
75
76
    /**
77
     * Get remaining length inside box for another item.
78
     */
79 1
    public function getRemainingLength(): int
80
    {
81 1
        return $this->box->getInnerLength() - $this->getUsedLength();
82
    }
83
84
    /**
85
     * Get remaining depth inside box for another item.
86
     */
87 1
    public function getRemainingDepth(): int
88
    {
89 1
        return $this->box->getInnerDepth() - $this->getUsedDepth();
90
    }
91
92
    /**
93
     * Used width inside box for packing items.
94
     */
95 5
    public function getUsedWidth(): int
96
    {
97 5
        $maxWidth = 0;
98
99 5
        foreach ($this->items as $item) {
100 5
            $maxWidth = max($maxWidth, $item->getX() + $item->getWidth());
101
        }
102
103 5
        return $maxWidth;
104
    }
105
106
    /**
107
     * Used length inside box for packing items.
108
     */
109 5
    public function getUsedLength(): int
110
    {
111 5
        $maxLength = 0;
112
113 5
        foreach ($this->items as $item) {
114 5
            $maxLength = max($maxLength, $item->getY() + $item->getLength());
115
        }
116
117 5
        return $maxLength;
118
    }
119
120
    /**
121
     * Used depth inside box for packing items.
122
     */
123 5
    public function getUsedDepth(): int
124
    {
125 5
        $maxDepth = 0;
126
127 5
        foreach ($this->items as $item) {
128 5
            $maxDepth = max($maxDepth, $item->getZ() + $item->getDepth());
129
        }
130
131 5
        return $maxDepth;
132
    }
133
134
    /**
135
     * Get remaining weight inside box for another item.
136
     */
137 1
    public function getRemainingWeight(): int
138
    {
139 1
        return $this->box->getMaxWeight() - $this->getWeight();
140
    }
141
142 99
    public function getInnerVolume(): int
143
    {
144 99
        return $this->box->getInnerWidth() * $this->box->getInnerLength() * $this->box->getInnerDepth();
145
    }
146
147
    /**
148
     * Get used volume of the packed box.
149
     */
150 99
    public function getUsedVolume(): int
151
    {
152 99
        return $this->items->getVolume();
153
    }
154
155
    /**
156
     * Get unused volume of the packed box.
157
     */
158 1
    public function getUnusedVolume(): int
159
    {
160 1
        return $this->getInnerVolume() - $this->getUsedVolume();
161
    }
162
163
    /**
164
     * Get volume utilisation of the packed box.
165
     */
166 32
    public function getVolumeUtilisation(): float
167
    {
168 32
        return $this->volumeUtilisation;
169
    }
170
171
    /**
172
     * Create a custom website visualiser URL for this packing.
173
     */
174 1
    public function generateVisualisationURL(): string
175
    {
176 1
        return 'https://boxpacker.io/en/master/visualiser.html?packing=' . urlencode(json_encode($this));
177
    }
178
179 99
    public function __construct(protected Box $box, protected PackedItemList $items)
180
    {
181 99
        foreach ($this->items as $item) {
182 96
            $this->itemWeight += $item->getItem()->getWeight();
183
        }
184 99
        $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...
185 99
        $this->assertPackingCompliesWithRealWorld();
186
    }
187
188 4
    public function jsonSerialize(): array
189
    {
190 4
        $userValues = [];
191
192 4
        if ($this->box instanceof JsonSerializable) {
193 3
            $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

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