Completed
Push — 2.x-dev ( f6da7f...8d94cf )
by Doug
19:11
created

Packer::getMaxBoxesToBalanceWeight()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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
    /**
24
     * Number of boxes at which balancing weight is deemed not worth it
25
     * @var int
26
     */
27
    protected $maxBoxesToBalanceWeight = 12;
28
29
    /**
30
     * List of items to be packed
31
     * @var ItemList
32
     */
33
    protected $items;
34
35
    /**
36
     * List of box sizes available to pack items into
37
     * @var BoxList
38
     */
39
    protected $boxes;
40
41
    /**
42
     * Constructor
43
     */
44 30
    public function __construct()
45
    {
46 30
        $this->items = new ItemList();
47 30
        $this->boxes = new BoxList();
48
49 30
        $this->logger = new NullLogger();
50
    }
51
52
    /**
53
     * Add item to be packed
54
     * @param Item $item
55
     * @param int  $qty
56
     */
57 29
    public function addItem(Item $item, $qty = 1)
58
    {
59 29
        for ($i = 0; $i < $qty; $i++) {
60 29
            $this->items->insert($item);
61
        }
62 29
        $this->logger->log(LogLevel::INFO, "added {$qty} x {$item->getDescription()}");
63
    }
64
65
    /**
66
     * Set a list of items all at once
67
     * @param \Traversable|array $items
68
     */
69 2
    public function setItems($items)
70
    {
71 2
        if ($items instanceof ItemList) {
72
            $this->items = clone $items;
73
        } else {
74 2
            $this->items = new ItemList();
75 2
            foreach ($items as $item) {
76 2
                $this->items->insert($item);
77
            }
78
        }
79
    }
80
81
    /**
82
     * Add box size
83
     * @param Box $box
84
     */
85 28
    public function addBox(Box $box)
86
    {
87 28
        $this->boxes->insert($box);
88 28
        $this->logger->log(LogLevel::INFO, "added box {$box->getReference()}");
89
    }
90
91
    /**
92
     * Add a pre-prepared set of boxes all at once
93
     * @param BoxList $boxList
94
     */
95 2
    public function setBoxes(BoxList $boxList)
96
    {
97 2
        $this->boxes = clone $boxList;
98
    }
99
100
    /**
101
     * Number of boxes at which balancing weight is deemed not worth the extra computation time
102
     * @return int
103
     */
104 1
    public function getMaxBoxesToBalanceWeight()
105
    {
106 1
        return $this->maxBoxesToBalanceWeight;
107
    }
108
109
    /**
110
     * Number of boxes at which balancing weight is deemed not worth the extra computation time
111
     * @param int $maxBoxesToBalanceWeight
112
     */
113 2
    public function setMaxBoxesToBalanceWeight($maxBoxesToBalanceWeight)
114
    {
115 2
        $this->maxBoxesToBalanceWeight = $maxBoxesToBalanceWeight;
116
    }
117
118
    /**
119
     * Pack items into boxes
120
     *
121
     * @return PackedBoxList
122
     */
123 29
    public function pack()
124
    {
125 29
        $packedBoxes = $this->doVolumePacking();
126
127
        //If we have multiple boxes, try and optimise/even-out weight distribution
128 27
        if ($packedBoxes->count() > 1 && $packedBoxes->count() <= $this->maxBoxesToBalanceWeight) {
129 6
            $redistributor = new WeightRedistributor($this->boxes);
130 6
            $redistributor->setLogger($this->logger);
131 6
            $packedBoxes = $redistributor->redistributeWeight($packedBoxes);
132
        }
133
134 27
        $this->logger->log(LogLevel::INFO, "packing completed, {$packedBoxes->count()} boxes");
135 27
        return $packedBoxes;
136
    }
137
138
    /**
139
     * Pack items into boxes using the principle of largest volume item first
140
     *
141
     * @throws ItemTooLargeException
142
     * @return PackedBoxList
143
     */
144 29
    public function doVolumePacking()
145
    {
146
147 29
        $packedBoxes = new PackedBoxList;
148
149
        //Keep going until everything packed
150 29
        while ($this->items->count()) {
151 29
            $boxesToEvaluate = clone $this->boxes;
152 29
            $packedBoxesIteration = new PackedBoxList;
153
154
            //Loop through boxes starting with smallest, see what happens
155 29
            while (!$boxesToEvaluate->isEmpty()) {
156 28
                $box = $boxesToEvaluate->extract();
157
158 28
                $volumePacker = new VolumePacker($box, clone $this->items);
159 28
                $volumePacker->setLogger($this->logger);
160 28
                $packedBox = $volumePacker->pack();
161 28
                if ($packedBox->getItems()->count()) {
162 28
                    $packedBoxesIteration->insert($packedBox);
163
164
                    //Have we found a single box that contains everything?
165 28
                    if ($packedBox->getItems()->count() === $this->items->count()) {
166 27
                        break;
167
                    }
168
                }
169
            }
170
171
            //Check iteration was productive
172 29
            if ($packedBoxesIteration->isEmpty()) {
173 2
                throw new ItemTooLargeException('Item ' . $this->items->top()->getDescription() . ' is too large to fit into any box', $this->items->top());
174
            }
175
176
            //Find best box of iteration, and remove packed items from unpacked list
177
            /** @var PackedBox $bestBox */
178 28
            $bestBox = $packedBoxesIteration->top();
179 28
            $unPackedItems = $this->items->asArray();
180 28
            foreach (clone $bestBox->getItems() as $packedItem) {
181 28
                foreach ($unPackedItems as $unpackedKey => $unpackedItem) {
182 28
                    if ($packedItem === $unpackedItem) {
183 28
                        unset($unPackedItems[$unpackedKey]);
184 28
                        break;
185
                    }
186
                }
187
            }
188 28
            $unpackedItemList = new ItemList();
189 28
            foreach ($unPackedItems as $unpackedItem) {
190 8
                $unpackedItemList->insert($unpackedItem);
191
            }
192 28
            $this->items = $unpackedItemList;
193 28
            $packedBoxes->insert($bestBox);
194
195
        }
196
197 27
        return $packedBoxes;
198
    }
199
}
200