Completed
Push — master ( 9610e0...320eee )
by Doug
08:43
created

Packer::setItems()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
crap 3.0416
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 26
    public function __construct()
41
    {
42 26
        $this->items = new ItemList();
43 26
        $this->boxes = new BoxList();
44
45 26
        $this->logger = new NullLogger();
46
    }
47
48
    /**
49
     * Add item to be packed
50
     * @param Item $item
51
     * @param int  $qty
52
     */
53 26
    public function addItem(Item $item, $qty = 1)
54
    {
55 26
        for ($i = 0; $i < $qty; $i++) {
56 26
            $this->items->insert($item);
57
        }
58 26
        $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 25
    public function addBox(Box $box)
82
    {
83 25
        $this->boxes->insert($box);
84 25
        $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 26
    public function pack()
102
    {
103 26
        $packedBoxes = $this->doVolumePacking();
104
105
        //If we have multiple boxes, try and optimise/even-out weight distribution
106 24
        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 24
        $this->logger->log(LogLevel::INFO, "packing completed, {$packedBoxes->count()} boxes");
113 24
        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 26
    public function doVolumePacking()
123
    {
124
125 26
        $packedBoxes = new PackedBoxList;
126
127
        //Keep going until everything packed
128 26
        while ($this->items->count()) {
129 26
            $boxesToEvaluate = clone $this->boxes;
130 26
            $packedBoxesIteration = new PackedBoxList;
131
132
            //Loop through boxes starting with smallest, see what happens
133 26
            while (!$boxesToEvaluate->isEmpty()) {
134 25
                $box = $boxesToEvaluate->extract();
135
136 25
                $volumePacker = new VolumePacker($box, clone $this->items);
137 25
                $volumePacker->setLogger($this->logger);
138 25
                $packedBox = $volumePacker->pack();
139 25
                if ($packedBox->getItems()->count()) {
140 25
                    $packedBoxesIteration->insert($packedBox);
141
142
                    //Have we found a single box that contains everything?
143 25
                    if ($packedBox->getItems()->count() === $this->items->count()) {
144 24
                        break;
145
                    }
146
                }
147
            }
148
149
            //Check iteration was productive
150 26
            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 25
            $bestBox = $packedBoxesIteration->top();
157 25
            $unPackedItems = $this->items->asArray();
158 25
            foreach (clone $bestBox->getItems() as $packedItem) {
159 25
                foreach ($unPackedItems as $unpackedKey => $unpackedItem) {
160 25
                    if ($packedItem->getItem() === $unpackedItem) {
161 25
                        unset($unPackedItems[$unpackedKey]);
162 25
                        break;
163
                    }
164
                }
165
            }
166 25
            $unpackedItemList = new ItemList();
167 25
            foreach ($unPackedItems as $unpackedItem) {
168 6
                $unpackedItemList->insert($unpackedItem);
169
            }
170 25
            $this->items = $unpackedItemList;
171 25
            $packedBoxes->insert($bestBox);
172
173
        }
174
175 24
        return $packedBoxes;
176
    }
177
}
178