Passed
Push — 2.x-dev ( e5166d...a75e6c )
by Doug
02:18
created

Packer::setMaxBoxesToBalanceWeight()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 13
    public function __construct()
49
    {
50 13
        $this->items = new ItemList();
51 13
        $this->boxes = new BoxList();
52
53 13
        $this->logger = new NullLogger();
54 13
    }
55
56
    /**
57
     * Add item to be packed.
58
     *
59
     * @param Item $item
60
     * @param int  $qty
61
     */
62 7
    public function addItem(Item $item, $qty = 1)
63
    {
64 7
        for ($i = 0; $i < $qty; $i++) {
65 7
            $this->items->insert($item);
66
        }
67 7
        $this->logger->log(LogLevel::INFO, "added {$qty} x {$item->getDescription()}");
68 7
    }
69
70
    /**
71
     * Set a list of items all at once.
72
     *
73
     * @param \Traversable|array $items
74
     */
75 7
    public function setItems($items)
76
    {
77 7
        if ($items instanceof ItemList) {
78 5
            $this->items = clone $items;
79
        } else {
80 2
            $this->items = new ItemList();
81 2
            foreach ($items as $item) {
82 2
                $this->items->insert($item);
83
            }
84
        }
85 7
    }
86
87
    /**
88
     * Add box size.
89
     *
90
     * @param Box $box
91
     */
92 6
    public function addBox(Box $box)
93
    {
94 6
        $this->boxes->insert($box);
95 6
        $this->logger->log(LogLevel::INFO, "added box {$box->getReference()}");
96 6
    }
97
98
    /**
99
     * Add a pre-prepared set of boxes all at once.
100
     *
101
     * @param BoxList $boxList
102
     */
103 7
    public function setBoxes(BoxList $boxList)
104
    {
105 7
        $this->boxes = clone $boxList;
106 7
    }
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 12
    public function pack()
134
    {
135 12
        $packedBoxes = $this->doVolumePacking();
136
137
        //If we have multiple boxes, try and optimise/even-out weight distribution
138 10
        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 10
        $this->logger->log(LogLevel::INFO, "packing completed, {$packedBoxes->count()} boxes");
145
146 10
        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 12
    public function doVolumePacking()
157
    {
158 12
        $packedBoxes = new PackedBoxList();
159
160
        //Keep going until everything packed
161 12
        while ($this->items->count()) {
162 12
            $boxesToEvaluate = clone $this->boxes;
163 12
            $packedBoxesIteration = new PackedBoxList();
164
165
            //Loop through boxes starting with smallest, see what happens
166 12
            while (!$boxesToEvaluate->isEmpty()) {
167 11
                $box = $boxesToEvaluate->extract();
168
169 11
                $volumePacker = new VolumePacker($box, clone $this->items);
170 11
                $volumePacker->setLogger($this->logger);
171 11
                $packedBox = $volumePacker->pack();
172 11
                if ($packedBox->getItems()->count()) {
173 11
                    $packedBoxesIteration->insert($packedBox);
174
175
                    //Have we found a single box that contains everything?
176 11
                    if ($packedBox->getItems()->count() === $this->items->count()) {
177 10
                        break;
178
                    }
179
                }
180
            }
181
182
            //Check iteration was productive
183 12
            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 11
            $bestBox = $packedBoxesIteration->top();
190 11
            $unPackedItems = $this->items->asArray();
191 11
            foreach (clone $bestBox->getItems() as $packedItem) {
192 11
                foreach ($unPackedItems as $unpackedKey => $unpackedItem) {
193 11
                    if ($packedItem === $unpackedItem) {
194 11
                        unset($unPackedItems[$unpackedKey]);
195 11
                        break;
196
                    }
197
                }
198
            }
199 11
            $unpackedItemList = new ItemList();
200 11
            foreach ($unPackedItems as $unpackedItem) {
201 4
                $unpackedItemList->insert($unpackedItem);
202
            }
203 11
            $this->items = $unpackedItemList;
204 11
            $packedBoxes->insert($bestBox);
205
        }
206
207 10
        return $packedBoxes;
208
    }
209
}
210