Passed
Push — 1.x-dev ( 9106e4...cc1b76 )
by Doug
02:13
created

Packer::addItem()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

This method has been deprecated.

Loading history...
241
242
        return $packedBox->getItems();
243
    }
244
245
    /**
246
     * Given a solution set of packed boxes, repack them to achieve optimum weight distribution.
247
     *
248
     * @deprecated
249
     *
250
     * @param PackedBoxList $originalBoxes
251
     *
252
     * @return PackedBoxList
253
     */
254
    public function redistributeWeight(PackedBoxList $originalBoxes)
255
    {
256
        $redistributor = new WeightRedistributor($this->boxes);
257
        $redistributor->setLogger($this->logger);
258
259
        return $redistributor->redistributeWeight($originalBoxes);
260
    }
261
}
262