Completed
Push — 1.x-dev ( f03c49...17b59f )
by Doug
68:27 queued 64:38
created

Packer::doVolumePacking()   B

Complexity

Conditions 10
Paths 37

Size

Total Lines 54
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 10.0036

Importance

Changes 0
Metric Value
cc 10
eloc 29
nc 37
nop 0
dl 0
loc 54
ccs 29
cts 30
cp 0.9667
crap 10.0036
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\LoggerAwareTrait;
11
use Psr\Log\LogLevel;
12
use Psr\Log\NullLogger;
13
14
/**
15
 * Actual packer.
16
 *
17
 * @author Doug Wright
18
 */
19
class Packer implements LoggerAwareInterface
20
{
21
    use LoggerAwareTrait;
22
23
    /**
24
     * Number of boxes at which balancing weight is deemed not worth it.
25
     *
26
     * @var int
27
     */
28
    protected $maxBoxesToBalanceWeight = 12;
29
30
    /**
31
     * List of items to be packed.
32
     *
33
     * @var ItemList
34
     */
35
    protected $items;
36
37
    /**
38
     * List of box sizes available to pack items into.
39
     *
40
     * @var BoxList
41
     */
42
    protected $boxes;
43
44
    /**
45
     * Constructor.
46
     */
47 20
    public function __construct()
48
    {
49 20
        $this->items = new ItemList();
50 20
        $this->boxes = new BoxList();
51
52 20
        $this->logger = new NullLogger();
53 20
    }
54
55
    /**
56
     * Add item to be packed.
57
     *
58
     * @param Item $item
59
     * @param int  $qty
60
     */
61 14
    public function addItem(Item $item, $qty = 1)
62
    {
63 14
        for ($i = 0; $i < $qty; ++$i) {
64 14
            $this->items->insert($item);
65
        }
66 14
        $this->logger->log(LogLevel::INFO, "added {$qty} x {$item->getDescription()}", ['item' => $item]);
67 14
    }
68
69
    /**
70
     * Set a list of items all at once.
71
     *
72
     * @param \Traversable|array $items
73
     */
74 8
    public function setItems($items)
75
    {
76 8
        if ($items instanceof ItemList) {
77 5
            $this->items = clone $items;
78
        } else {
79 3
            $this->items = new ItemList();
80 3
            foreach ($items as $item) {
81 3
                $this->items->insert($item);
82
            }
83
        }
84 8
    }
85
86
    /**
87
     * Add box size.
88
     *
89
     * @param Box $box
90
     */
91 13
    public function addBox(Box $box)
92
    {
93 13
        $this->boxes->insert($box);
94 13
        $this->logger->log(LogLevel::INFO, "added box {$box->getReference()}", ['box' => $box]);
95 13
    }
96
97
    /**
98
     * Add a pre-prepared set of boxes all at once.
99
     *
100
     * @param BoxList $boxList
101
     */
102 8
    public function setBoxes(BoxList $boxList)
103
    {
104 8
        $this->boxes = clone $boxList;
105 8
    }
106
107
    /**
108
     * Number of boxes at which balancing weight is deemed not worth the extra computation time.
109
     *
110
     * @return int
111
     */
112 1
    public function getMaxBoxesToBalanceWeight()
113
    {
114 1
        return $this->maxBoxesToBalanceWeight;
115
    }
116
117
    /**
118
     * Number of boxes at which balancing weight is deemed not worth the extra computation time.
119
     *
120
     * @param int $maxBoxesToBalanceWeight
121
     */
122 2
    public function setMaxBoxesToBalanceWeight($maxBoxesToBalanceWeight)
123
    {
124 2
        $this->maxBoxesToBalanceWeight = $maxBoxesToBalanceWeight;
125 2
    }
126
127
    /**
128
     * Pack items into boxes.
129
     *
130
     * @return PackedBoxList
131
     */
132 19
    public function pack()
133
    {
134 19
        $packedBoxes = $this->doVolumePacking();
135
136
        //If we have multiple boxes, try and optimise/even-out weight distribution
137 17
        if ($packedBoxes->count() > 1 && $packedBoxes->count() <= $this->maxBoxesToBalanceWeight) {
138 7
            $redistributor = new WeightRedistributor($this->boxes);
139 7
            $redistributor->setLogger($this->logger);
140 7
            $packedBoxes = $redistributor->redistributeWeight($packedBoxes);
141
        }
142
143 17
        $this->logger->log(LogLevel::INFO, "[PACKING COMPLETED], {$packedBoxes->count()} boxes");
144
145 17
        return $packedBoxes;
146
    }
147
148
    /**
149
     * Pack items into boxes using the principle of largest volume item first.
150
     *
151
     * @throws ItemTooLargeException
152
     *
153
     * @return PackedBoxList
154
     */
155 19
    public function doVolumePacking()
156
    {
157 19
        $packedBoxes = new PackedBoxList();
158
159 19
        $this->sanityPrecheck();
160
161
        //Keep going until everything packed
162 17
        while ($this->items->count()) {
163 17
            $boxesToEvaluate = clone $this->boxes;
164 17
            $packedBoxesIteration = new PackedBoxList();
165
166
            //Loop through boxes starting with smallest, see what happens
167 17
            while (!$boxesToEvaluate->isEmpty()) {
168 17
                $box = $boxesToEvaluate->extract();
169
170 17
                $volumePacker = new VolumePacker($box, clone $this->items);
171 17
                $volumePacker->setLogger($this->logger);
172 17
                $packedBox = $volumePacker->pack();
173 17
                if ($packedBox->getItems()->count()) {
174 17
                    $packedBoxesIteration->insert($packedBox);
175
176
                    //Have we found a single box that contains everything?
177 17
                    if ($packedBox->getItems()->count() === $this->items->count()) {
178 17
                        break;
179
                    }
180
                }
181
            }
182
183
            //Check iteration was productive
184 17
            if ($packedBoxesIteration->isEmpty()) {
185
                throw new ItemTooLargeException('Item '.$this->items->top()->getDescription().' is too large to fit into any box', $this->items->top());
186
            }
187
188
            //Find best box of iteration, and remove packed items from unpacked list
189
            /** @var PackedBox $bestBox */
190 17
            $bestBox = $packedBoxesIteration->top();
191 17
            $unPackedItems = $this->items->asArray();
192 17
            foreach (clone $bestBox->getItems() as $packedItem) {
193 17
                foreach ($unPackedItems as $unpackedKey => $unpackedItem) {
194 17
                    if ($packedItem === $unpackedItem) {
195 17
                        unset($unPackedItems[$unpackedKey]);
196 17
                        break;
197
                    }
198
                }
199
            }
200 17
            $unpackedItemList = new ItemList();
201 17
            foreach ($unPackedItems as $unpackedItem) {
202 7
                $unpackedItemList->insert($unpackedItem);
203
            }
204 17
            $this->items = $unpackedItemList;
205 17
            $packedBoxes->insert($bestBox);
206
        }
207
208 17
        return $packedBoxes;
209
    }
210
211
    /**
212
     * Pack as many items as possible into specific given box.
213
     *
214
     * @deprecated
215
     *
216
     * @param Box      $box
217
     * @param ItemList $items
218
     *
219
     * @return PackedBox packed box
220
     */
221
    public function packIntoBox(Box $box, ItemList $items)
222
    {
223
        $volumePacker = new VolumePacker($box, clone $items);
224
        $volumePacker->setLogger($this->logger);
225
226
        return $volumePacker->pack();
227
    }
228
229
    /**
230
     * Pack as many items as possible into specific given box.
231
     *
232
     * @deprecated
233
     *
234
     * @param Box      $box
235
     * @param ItemList $items
236
     *
237
     * @return ItemList items packed into box
238
     */
239
    public function packBox(Box $box, ItemList $items)
240
    {
241
        $packedBox = $this->packIntoBox($box, $items);
0 ignored issues
show
Deprecated Code introduced by
The function DVDoug\BoxPacker\Packer::packIntoBox() has been deprecated. ( Ignorable by Annotation )

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

241
        $packedBox = /** @scrutinizer ignore-deprecated */ $this->packIntoBox($box, $items);
Loading history...
242
243
        return $packedBox->getItems();
244
    }
245
246
    /**
247
     * Given a solution set of packed boxes, repack them to achieve optimum weight distribution.
248
     *
249
     * @deprecated
250
     *
251
     * @param PackedBoxList $originalBoxes
252
     *
253
     * @return PackedBoxList
254
     */
255
    public function redistributeWeight(PackedBoxList $originalBoxes)
256
    {
257
        $redistributor = new WeightRedistributor($this->boxes);
258
        $redistributor->setLogger($this->logger);
259
260
        return $redistributor->redistributeWeight($originalBoxes);
261
    }
262
263 19
    private function sanityPrecheck()
264
    {
265
        /** @var Item $item */
266 19
        foreach (clone $this->items as $item) {
267 19
            $possibleFits = 0;
268
            /** @var Box $box */
269 19
            foreach (clone $this->boxes as $box) {
270 18
                if ($item->getWeight() <= ($box->getMaxWeight() - $box->getEmptyWeight())) {
271 18
                    $possibleFits += count((new OrientatedItemFactory($box))->getPossibleOrientationsInEmptyBox($item));
272
                }
273
            }
274 19
            if ($possibleFits === 0) {
275 2
                throw new ItemTooLargeException('Item ' . $item->getDescription() . ' is too large to fit into any box', $item);
276
            }
277
        }
278 17
    }
279
}
280