Completed
Push — 1.x-dev ( e51b9c )
by Doug
09:00
created

Packer.php (1 issue)

Upgrade to new PHP Analysis Engine

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
Deprecated Code introduced by
The method DVDoug\BoxPacker\Packer::packIntoBox() has been deprecated.

This method has been deprecated.

Loading history...
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