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