Test Failed
Push — 2.x-dev ( 5e90af...eccc16 )
by Doug
03:16
created

VolumePacker::tryAndStackItemsIntoSpace()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 21
c 0
b 0
f 0
nc 3
nop 10
dl 0
loc 34
ccs 21
cts 21
cp 1
crap 4
rs 9.584

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
 * Box packing (3D bin packing, knapsack problem).
4
 *
5
 * @author Doug Wright
6
 */
7
namespace DVDoug\BoxPacker;
8
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LoggerInterface;
11
use Psr\Log\NullLogger;
12
13
/**
14
 * Actual packer.
15
 *
16
 * @author Doug Wright
17
 */
18
class VolumePacker implements LoggerAwareInterface
19
{
20
    /**
21
     * The logger instance.
22
     *
23
     * @var LoggerInterface
24
     */
25
    protected $logger;
26
27
    /**
28
     * Box to pack items into.
29
     *
30
     * @var Box
31
     */
32
    protected $box;
33
34
    /**
35
     * List of items to be packed.
36
     *
37
     * @var ItemList
38
     */
39
    protected $items;
40
41
    /**
42
     * Whether the packer is in single-pass mode.
43
     *
44
     * @var bool
45
     */
46
    protected $singlePassMode = false;
47
48
    /**
49
     * @var LayerPacker
50
     */
51
    private $layerPacker;
52
53
    /**
54
     * @var bool
55
     */
56
    private $hasConstrainedItems;
57
58
    /**
59
     * Constructor.
60
     */
61 28
    public function __construct(Box $box, ItemList $items)
62
    {
63 28
        $this->box = $box;
64 28
        $this->items = clone $items;
65
66 28
        $this->logger = new NullLogger();
67
68 28
        $this->hasConstrainedItems = $items->hasConstrainedItems();
69
70 28
        $this->layerPacker = new LayerPacker($this->box);
71 28
        $this->layerPacker->setLogger($this->logger);
72 28
    }
73
74
    /**
75
     * Sets a logger.
76
     */
77 11
    public function setLogger(LoggerInterface $logger)
78
    {
79 11
        $this->logger = $logger;
80 11
        $this->layerPacker->setLogger($logger);
81 11
    }
82
83
    /**
84
     * @internal
85
     */
86 23
    public function setSinglePassMode($singlePassMode)
87
    {
88 23
        $this->singlePassMode = $singlePassMode;
89 23
        $this->layerPacker->setSinglePassMode($singlePassMode);
90 23
    }
91
92
    /**
93
     * Pack as many items as possible into specific given box.
94
     *
95
     * @return PackedBox packed box
96
     */
97 28
    public function pack()
98
    {
99 28
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);
100
101 28
        $rotationsToTest = [false];
102 28
        if (!$this->singlePassMode) {
103 28
            $rotationsToTest[] = true;
104
        }
105
106 28
        $boxPermutations = [];
107 28
        foreach ($rotationsToTest as $rotation) {
108 28
            if ($rotation) {
109 1
                $boxWidth = $this->box->getInnerLength();
110 1
                $boxLength = $this->box->getInnerWidth();
111
            } else {
112 28
                $boxWidth = $this->box->getInnerWidth();
113 28
                $boxLength = $this->box->getInnerLength();
114
            }
115
116 28
            $boxPermutation = $this->packRotation($boxWidth, $boxLength);
117 28
            if ($boxPermutation->getItems()->count() === $this->items->count()) {
118 14
                return $boxPermutation;
119
            }
120
121 15
            $boxPermutations[] = $boxPermutation;
122
        }
123
124
        usort($boxPermutations, static function (PackedBox $a, PackedBox $b) {
125 1
            if ($a->getVolumeUtilisation() < $b->getVolumeUtilisation()) {
126
                return 1;
127
            }
128 1
            return -1;
129 15
        });
130
131 15
        return reset($boxPermutations);
132
    }
133
134
    /**
135
     * Pack as many items as possible into specific given box.
136
     *
137
     * @return PackedBox packed box
138
     */
139 28
    private function packRotation($boxWidth, $boxLength)
140
    {
141 28
        $this->logger->debug("[EVALUATING ROTATION] {$this->box->getReference()}", ['width' => $boxWidth, 'length' => $boxLength]);
142
143
        /** @var PackedLayer[] $layers */
144 28
        $layers = [];
145 28
        $items = clone $this->items;
146
147 28
        while ($items->count() > 0) {
148 28
            $layerStartDepth = static::getCurrentPackedDepth($layers);
149 28
            $packedItemList = $this->getPackedItemList($layers);
150
151
            //do a preliminary layer pack to get the depth used
152 28
            $preliminaryItems = clone $items;
153 28
            $preliminaryLayer = $this->layerPacker->packLayer($preliminaryItems, clone $packedItemList, $layers, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, 0);
154 28
            if (count($preliminaryLayer->getItems()) === 0) {
155 15
                break;
156
            }
157
158 18
            if ($preliminaryLayer->getDepth() === $preliminaryLayer->getItems()[0]->getDepth()) { // preliminary === final
159 18
                $layers[] = $preliminaryLayer;
160 18
                $items = $preliminaryItems;
161
            } else { //redo with now-known-depth so that we can stack to that height from the first item
162 1
                $layers[] = $this->layerPacker->packLayer($items, $packedItemList, $layers, $layerStartDepth, $boxWidth, $boxLength, $this->box->getInnerDepth() - $layerStartDepth, $preliminaryLayer->getDepth());
163
            }
164
        }
165
166 28
        $layers = $this->correctLayerRotation($layers, $boxWidth);
167 28
        $layers = $this->stabiliseLayers($layers);
168
169 28
        return PackedBox::fromPackedItemList($this->box, $this->getPackedItemList($layers));
170
    }
171
172
    /**
173
     * During packing, it is quite possible that layers have been created that aren't physically stable
174
     * i.e. they overhang the ones below.
175
     *
176
     * This function reorders them so that the ones with the greatest surface area are placed at the bottom
177
     *
178
     * @param  PackedLayer[] $oldLayers
179
     * @return PackedLayer[]
180
     */
181 28
    private function stabiliseLayers(array $oldLayers)
182
    {
183 28
        if ($this->singlePassMode || $this->hasConstrainedItems) { // constraints include position, so cannot change
184 17
            return $oldLayers;
185
        }
186
187 13
        $stabiliser = new LayerStabiliser();
188
189 13
        return $stabiliser->stabilise($oldLayers);
190
    }
191
192
    /**
193
     * Swap back width/length of the packed items to match orientation of the box if needed.
194
     *
195
     * @param PackedLayer[] $oldLayers
196
     */
197 28
    private function correctLayerRotation(array $oldLayers, $boxWidth)
198
    {
199 28
        if ($this->box->getInnerWidth() === $boxWidth) {
200 28
            return $oldLayers;
201
        }
202
203
        $newLayers = [];
204
        foreach ($oldLayers as $originalLayer) {
205
            $newLayer = new PackedLayer();
206
            foreach ($originalLayer->getItems() as $item) {
207
                $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth());
208
                $newLayer->insert($packedItem);
209
            }
210
            $newLayers[] = $newLayer;
211
        }
212
213
        return $newLayers;
214
    }
215
216
    /**
217
     * Generate a single list of items packed.
218
     * @param PackedLayer[] $layers
219
     */
220 28
    private function getPackedItemList(array $layers)
221
    {
222 28
        $packedItemList = new PackedItemList();
223 28
        foreach ($layers as $layer) {
224 18
            foreach ($layer->getItems() as $packedItem) {
225 18
                $packedItemList->insert($packedItem);
226
            }
227
        }
228
229 28
        return $packedItemList;
230
    }
231
232
    /**
233
     * Return the current packed depth.
234
     *
235
     * @param PackedLayer[] $layers
236
     */
237 28
    private static function getCurrentPackedDepth(array $layers)
238
    {
239 28
        $depth = 0;
240 28
        foreach ($layers as $layer) {
241 10
            $depth += $layer->getDepth();
242
        }
243
244 28
        return $depth;
245
    }
246
}
247