OrientatedItemSorter::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
c 2
b 0
f 0
nc 1
nop 12
dl 0
loc 14
ccs 1
cts 1
cp 1
crap 1
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/**
4
 * Box packing (3D bin packing, knapsack problem).
5
 *
6
 * @author Doug Wright
7
 */
8
declare(strict_types=1);
9
10
namespace DVDoug\BoxPacker;
11
12
use Psr\Log\LoggerInterface;
13
14
use function max;
15
use function min;
16
17
use const PHP_INT_MAX;
18
19
/**
20
 * Figure out best choice of orientations for an item and a given context.
21
 * @internal
22
 */
23
class OrientatedItemSorter
24
{
25
    /**
26
     * @var array<string, int>
27
     */
28
    protected static array $lookaheadCache = [];
29 86
30
    public function __construct(
31
        private readonly OrientatedItemFactory $orientatedItemFactory,
32
        private readonly bool $singlePassMode,
33
        private readonly int $widthLeft,
34
        private readonly int $lengthLeft,
35
        private readonly int $depthLeft,
36
        private readonly ItemList $nextItems,
37
        private readonly int $rowLength,
38
        private readonly int $x,
39
        private readonly int $y,
40
        private readonly int $z,
41
        private readonly PackedItemList $prevPackedItemList,
42
        private readonly LoggerInterface $logger
43 86
    ) {
44
    }
45 36
46
    public function __invoke(OrientatedItem $a, OrientatedItem $b): int
47
    {
48 36
        // Prefer exact fits in width/length/depth order
49 36
        $orientationAWidthLeft = $this->widthLeft - $a->width;
50 36
        $orientationBWidthLeft = $this->widthLeft - $b->width;
51 36
        $widthDecider = $this->exactFitDecider($orientationAWidthLeft, $orientationBWidthLeft);
52 13
        if ($widthDecider !== 0) {
53
            return $widthDecider;
54
        }
55 33
56 33
        $orientationALengthLeft = $this->lengthLeft - $a->length;
57 33
        $orientationBLengthLeft = $this->lengthLeft - $b->length;
58 33
        $lengthDecider = $this->exactFitDecider($orientationALengthLeft, $orientationBLengthLeft);
59 21
        if ($lengthDecider !== 0) {
60
            return $lengthDecider;
61
        }
62 30
63 30
        $orientationADepthLeft = $this->depthLeft - $a->depth;
64 30
        $orientationBDepthLeft = $this->depthLeft - $b->depth;
65 30
        $depthDecider = $this->exactFitDecider($orientationADepthLeft, $orientationBDepthLeft);
66 4
        if ($depthDecider !== 0) {
67
            return $depthDecider;
68
        }
69
70 30
        // prefer leaving room for next item(s)
71 30
        $followingItemDecider = $this->lookAheadDecider($a, $b, $orientationAWidthLeft, $orientationBWidthLeft);
72 24
        if ($followingItemDecider !== 0) {
73
            return $followingItemDecider;
74
        }
75
76 24
        // otherwise prefer leaving minimum possible gap, or the greatest footprint
77 24
        $orientationAMinGap = min($orientationAWidthLeft, $orientationALengthLeft);
78
        $orientationBMinGap = min($orientationBWidthLeft, $orientationBLengthLeft);
79 24
80
        return $orientationAMinGap <=> $orientationBMinGap ?: $a->surfaceFootprint <=> $b->surfaceFootprint;
81
    }
82 30
83
    private function lookAheadDecider(OrientatedItem $a, OrientatedItem $b, int $orientationAWidthLeft, int $orientationBWidthLeft): int
84 30
    {
85 18
        if ($this->nextItems->count() === 0) {
86
            return 0;
87
        }
88 26
89 26
        $nextItemFitA = $this->orientatedItemFactory->getPossibleOrientations($this->nextItems->top(), $a, $orientationAWidthLeft, $this->lengthLeft, $this->depthLeft, $this->x, $this->y, $this->z, $this->prevPackedItemList);
90 26
        $nextItemFitB = $this->orientatedItemFactory->getPossibleOrientations($this->nextItems->top(), $b, $orientationBWidthLeft, $this->lengthLeft, $this->depthLeft, $this->x, $this->y, $this->z, $this->prevPackedItemList);
91 10
        if ($nextItemFitA && !$nextItemFitB) {
92
            return -1;
93 25
        }
94 19
        if ($nextItemFitB && !$nextItemFitA) {
95
            return 1;
96
        }
97
98 20
        // if not an easy either/or, do a partial lookahead
99 20
        $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a);
100
        $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b);
101 20
102
        return $additionalPackedB <=> $additionalPackedA ?: 0;
103
    }
104
105
    /**
106
     * Approximation of a forward-looking packing.
107
     *
108
     * Not an actual packing, that has additional logic regarding constraints and stackability, this focuses
109
     * purely on fit.
110 20
     */
111
    protected function calculateAdditionalItemsPackedWithThisOrientation(
112
        OrientatedItem $prevItem
113 20
    ): int {
114 14
        if ($this->singlePassMode) {
115
            return 0;
116
        }
117 20
118
        $currentRowLength = max($prevItem->length, $this->rowLength);
119 20
120
        $itemsToPack = $this->nextItems->topN(8); // cap lookahead as this gets recursive and slow
121 20
122 20
        $cacheKey = $this->widthLeft .
123 20
            '|' .
124 20
            $this->lengthLeft .
125 20
            '|' .
126 20
            $prevItem->width .
127 20
            '|' .
128 20
            $prevItem->length .
129 20
            '|' .
130 20
            $currentRowLength .
131 20
            '|'
132
            . $this->depthLeft;
133 20
134 20
        foreach ($itemsToPack as $itemToPack) {
135 20
            $cacheKey .= '|' .
136 20
                $itemToPack->getWidth() .
137 20
                '|' .
138 20
                $itemToPack->getLength() .
139 20
                '|' .
140 20
                $itemToPack->getDepth() .
141 20
                '|' .
142 20
                $itemToPack->getWeight() .
143 20
                '|' .
144
                $itemToPack->getAllowedRotation()->name;
145
        }
146 20
147 19
        if (!isset(static::$lookaheadCache[$cacheKey])) {
148 19
            $tempBox = new WorkingVolume($this->widthLeft - $prevItem->width, $currentRowLength, $this->depthLeft, PHP_INT_MAX);
149 19
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
150 19
            $tempPacker->setSinglePassMode(true);
151
            $remainingRowPacked = $tempPacker->pack();
152 19
153
            $itemsToPack->removePackedItems($remainingRowPacked->items);
154 19
155 19
            $tempBox = new WorkingVolume($this->widthLeft, $this->lengthLeft - $currentRowLength, $this->depthLeft, PHP_INT_MAX);
156 19
            $tempPacker = new VolumePacker($tempBox, $itemsToPack);
157 19
            $tempPacker->setSinglePassMode(true);
158
            $nextRowsPacked = $tempPacker->pack();
159 19
160
            $itemsToPack->removePackedItems($nextRowsPacked->items);
161 19
162 19
            $packedCount = $this->nextItems->count() - $itemsToPack->count();
163
            $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]);
164 19
165
            static::$lookaheadCache[$cacheKey] = $packedCount;
166
        }
167 20
168
        return static::$lookaheadCache[$cacheKey];
169
    }
170 36
171
    private function exactFitDecider(int $dimensionALeft, int $dimensionBLeft): int
172 36
    {
173 18
        if ($dimensionALeft === 0 && $dimensionBLeft > 0) {
174
            return -1;
175
        }
176 34
177 18
        if ($dimensionALeft > 0 && $dimensionBLeft === 0) {
178
            return 1;
179
        }
180 33
181
        return 0;
182
    }
183
}
184