Completed
Push — master ( 94ac26...eb6c4c )
by Doug
03:14
created

VolumePacker::fitsBetterUnrotated()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 9
rs 8.8571
ccs 3
cts 3
cp 1
cc 6
eloc 6
nc 10
nop 4
crap 6
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 VolumePacker implements LoggerAwareInterface
20
{
21
    use LoggerAwareTrait;
22
23
    /**
24
     * Box to pack items into
25
     * @var Box
26
     */
27
    protected $box;
28
29
    /**
30
     * List of items to be packed
31
     * @var ItemList
32
     */
33
    protected $items;
34
35
    /**
36
     * Constructor
37
     */
38 23
    public function __construct(Box $box, ItemList $items)
39
    {
40 23
        $this->box = $box;
41 23
        $this->items = $items;
42 23
        $this->logger = new NullLogger();
43 23
    }
44
45
    /**
46
     * Pack as many items as possible into specific given box
47
     * @return PackedBox packed box
48
     */
49 23
    public function pack()
50
    {
51 23
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
52
53 23
        $packedItems = new ItemList;
54 23
        $depthLeft = $this->box->getInnerDepth();
55 23
        $remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
56 23
        $widthLeft = $this->box->getInnerWidth();
57 23
        $lengthLeft = $this->box->getInnerLength();
58
59 23
        $layerWidth = $layerLength = $layerDepth = 0;
60 23
61
        $prevItem = null;
62 23
63
        while (!$this->items->isEmpty()) {
64
65 23
            $itemToPack = $this->items->extract();
66 8
67 8
            //skip items that are simply too heavy
68
            if ($itemToPack->getWeight() > $remainingWeight) {
69
                continue;
70 23
            }
71 23
72 23
            $this->logger->debug("evaluating item {$itemToPack->getDescription()}");
73
            $this->logger->debug("remaining width: {$widthLeft}, length: {$lengthLeft}, depth: {$depthLeft}");
74 23
            $this->logger->debug("layerWidth: {$layerWidth}, layerLength: {$layerLength}, layerDepth: {$layerDepth}");
75 23
76
            $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
77 23
            $orientatedItem = $this->findBestOrientation($itemToPack, $prevItem, $nextItem, $widthLeft, $lengthLeft, $depthLeft);
78
79 23
            if ($orientatedItem) {
80 23
81
                $packedItems->insert($orientatedItem->getItem());
82 23
                $remainingWeight -= $itemToPack->getWeight();
83 23
84 22
                $lengthLeft -= $orientatedItem->getLength();
85 22
                $layerLength += $orientatedItem->getLength();
86 22
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
87 22
88 22
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
89 8
90 8
                //allow items to be stacked in place within the same footprint up to current layerdepth
91 8
                $maxStackDepth = $layerDepth - $orientatedItem->getDepth();
92 8
                while (!$this->items->isEmpty() && $this->canStackItemInLayer($itemToPack, $this->items->top(), $maxStackDepth, $remainingWeight)) {
93
                    $remainingWeight -= $this->items->top()->getWeight();
94 23
                    $maxStackDepth -= $this->items->top()->getDepth(); // XXX no attempt at best fit
95
                    $packedItems->insert($this->items->extract());
96
                }
97 23
98 23
                $prevItem = $orientatedItem;
99 1
            } else {
100 1
101 1
                $prevItem = null;
102 1
103 23
                if ($widthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
104 19
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
105 19
                    $lengthLeft += $layerLength;
106 19
                    $widthLeft -= $layerWidth;
107 19
                    $layerWidth = $layerLength = 0;
108 19
                    $this->items->insert($itemToPack);
109 19
                    continue;
110 17
                } elseif ($lengthLeft < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
111 2
                    $this->logger->debug("doesn't fit on layer even when empty");
112 2
                    continue;
113 2
                }
114
115
                $widthLeft = $layerWidth ? min(floor($layerWidth * 1.1), $this->box->getInnerWidth()) : $this->box->getInnerWidth();
116 17
                $lengthLeft = $layerLength ? min(floor($layerLength * 1.1), $this->box->getInnerLength()) : $this->box->getInnerLength();
117 17
                $depthLeft -= $layerDepth;
118 17
119
                $layerWidth = $layerLength = $layerDepth = 0;
120 17
                $this->logger->debug("doesn't fit, so starting next vertical layer");
121 17
                $this->items->insert($itemToPack);
122
            }
123 23
        }
124 23
        $this->logger->debug("done with this box");
125 23
        return new PackedBox($this->box, $packedItems, $widthLeft, $lengthLeft, $depthLeft, $remainingWeight);
126
    }
127
128
    /**
129
     * Figure out space left for next item if we pack this one in it's regular orientation
130
     * @param Item $item
131
     * @param int $widthLeft
132
     * @param int $lengthLeft
133
     * @return int
134 23
     */
135 23
    protected function fitsSameGap(Item $item, $widthLeft, $lengthLeft) {
136
        return min($widthLeft - $item->getWidth(), $lengthLeft - $item->getLength());
137
    }
138
139
    /**
140
     * Figure out space left for next item if we pack this one rotated by 90deg
141
     * @param Item $item
142
     * @param int $widthLeft
143
     * @param int $lengthLeft
144
     * @return int
145 23
     */
146 23
    protected function fitsRotatedGap(Item $item, $widthLeft, $lengthLeft) {
147
        return min($widthLeft - $item->getLength(), $lengthLeft - $item->getWidth());
148
    }
149
150
    /**
151
     * Get the best orientation for an item
152
     * @param Item $item
153
     * @param OrientatedItem|null $prevItem
154
     * @param Item|null $nextItem
155
     * @param int $widthLeft
156 23
     * @param int $lengthLeft
157 23
     * @param int $depthLeft
158
     * @return OrientatedItem|false
159
     */
160
    protected function findBestOrientation(Item $item, OrientatedItem $prevItem = null, Item $nextItem = null, $widthLeft, $lengthLeft, $depthLeft) {
161
162
        //Special case items that are the same as what we just packed - keep orientation
163
        if ($prevItem && $prevItem->getItem() == $item) {
164
            $orientatedItem = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
165
166
            if ($widthLeft - $orientatedItem->getWidth() >= 0 &&
167 23
                $lengthLeft - $orientatedItem->getLength() >= 0 &&
168
                $depthLeft - $orientatedItem->getDepth() >= 0) {
169 23
                return $orientatedItem;
170 23
            } else {
171
                return false;
172 23
            }
173 22
        }
174 23
175
176
        $fitsSameGap = $this->fitsSameGap($item, $widthLeft, $lengthLeft);
177
        $fitsRotatedGap = $this->fitsRotatedGap($item, $widthLeft, $lengthLeft);
178
        $fitsDepth = $item->getDepth() <= $depthLeft;
179
180
        $fitsAtAll = $fitsDepth && ($fitsSameGap >= 0 || $fitsRotatedGap >= 0);
181
182
        if (!$fitsAtAll) {
183
            return false;
184 23
        }
185 23
186 23
        $betterUnRotated = !!($fitsRotatedGap < 0 ||
187
            ($fitsSameGap >= 0 && $fitsSameGap <= $fitsRotatedGap) ||
188
            ($item->getWidth() <= $widthLeft && $nextItem == $item && $lengthLeft >= 2 * $item->getLength()));
189
190
        if ($betterUnRotated) {
191
            $this->logger->debug("fits (better) unrotated");
192
            return new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
193
        } else {
194
            $this->logger->debug("fits (better) rotated");
195
            return new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
196
        }
197
    }
198 22
199
    /**
200 22
     * Figure out if we can stack the next item vertically on top of this rather than side by side
201 22
     * Used when we've packed a tall item, and have just put a shorter one next to it
202 22
     * @param Item $item
203 22
     * @param Item $nextItem
204
     * @param $maxStackDepth
205
     * @param $remainingWeight
206
     * @return bool
207
     */
208
    protected function canStackItemInLayer(Item $item, Item $nextItem, $maxStackDepth, $remainingWeight)
209
    {
210
        return $nextItem->getDepth() <= $maxStackDepth &&
211
               $nextItem->getWeight() <= $remainingWeight &&
212 19
               $nextItem->getWidth() <= $item->getWidth() &&
213 19
               $nextItem->getLength() <= $item->getLength();
214
    }
215
216
    /**
217
     * @param $layerWidth
218
     * @param $layerLength
219
     * @param $layerDepth
220
     * @return bool
221
     */
222
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth) {
223
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
224
    }
225
}
226