Completed
Branch master (ca74d3)
by Doug
02:00
created

Packer   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 155
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 78
Bugs 6 Features 13
Metric Value
wmc 20
c 78
b 6
f 13
lcom 1
cbo 11
dl 0
loc 155
ccs 59
cts 59
cp 1
rs 10

7 Methods

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