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

VolumePacker::fitsRotatedGap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 3
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 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->log(LogLevel::DEBUG, "[EVALUATING BOX] {$this->box->getReference()}");
52
53 23
        $packedItems = new ItemList;
54 23
        $remainingDepth = $this->box->getInnerDepth();
55 23
        $remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
56 23
        $remainingWidth = $this->box->getInnerWidth();
57 23
        $remainingLength = $this->box->getInnerLength();
58
59 23
        $layerWidth = $layerLength = $layerDepth = 0;
60 23
        while (!$this->items->isEmpty()) {
61
62 23
            $itemToPack = $this->items->top();
63
64
            //skip items that are simply too large
65 23
            if ($this->isItemTooLargeForBox($itemToPack, $remainingDepth, $remainingWeight)) {
66 8
                $this->items->extract();
67 8
                continue;
68
            }
69
70 23
            $this->logger->log(LogLevel::DEBUG, "evaluating item {$itemToPack->getDescription()}");
71 23
            $this->logger->log(LogLevel::DEBUG, "remaining width: {$remainingWidth}, length: {$remainingLength}, depth: {$remainingDepth}");
72 23
            $this->logger->log(LogLevel::DEBUG, "layerWidth: {$layerWidth}, layerLength: {$layerLength}, layerDepth: {$layerDepth}");
73
74 23
            $itemWidth = $itemToPack->getWidth();
75 23
            $itemLength = $itemToPack->getLength();
76
77 23
            if ($this->fitsGap($itemToPack, $remainingWidth, $remainingLength)) {
78
79 23
                $packedItems->insert($this->items->extract());
80 23
                $remainingWeight -= $itemToPack->getWeight();
81
82 23
                $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
83 23
                if ($this->fitsBetterRotated($itemToPack, $nextItem, $remainingWidth, $remainingLength)) {
84 22
                    $this->logger->log(LogLevel::DEBUG, "fits (better) unrotated");
85 22
                    $remainingLength -= $itemLength;
86 22
                    $layerLength += $itemLength;
87 22
                    $layerWidth = max($itemWidth, $layerWidth);
88
                } else {
89 8
                    $this->logger->log(LogLevel::DEBUG, "fits (better) rotated");
90 8
                    $remainingLength -= $itemWidth;
91 8
                    $layerLength += $itemWidth;
92 8
                    $layerWidth = max($itemLength, $layerWidth);
93
                }
94 23
                $layerDepth = max($layerDepth, $itemToPack->getDepth()); //greater than 0, items will always be less deep
95
96
                //allow items to be stacked in place within the same footprint up to current layerdepth
97 23
                $maxStackDepth = $layerDepth - $itemToPack->getDepth();
98 23
                while (!$this->items->isEmpty() && $this->canStackItemInLayer($itemToPack, $this->items->top(), $maxStackDepth, $remainingWeight)) {
99 1
                    $remainingWeight -= $this->items->top()->getWeight();
100 1
                    $maxStackDepth -= $this->items->top()->getDepth();
101 1
                    $packedItems->insert($this->items->extract());
102
                }
103
            } else {
104 19
                if ($remainingWidth >= min($itemWidth, $itemLength) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
105 19
                    $this->logger->log(LogLevel::DEBUG, "No more fit in lengthwise, resetting for new row");
106 19
                    $remainingLength += $layerLength;
107 19
                    $remainingWidth -= $layerWidth;
108 19
                    $layerWidth = $layerLength = 0;
109 19
                    continue;
110 17
                } elseif ($remainingLength < min($itemWidth, $itemLength) || $layerDepth == 0) {
111 2
                    $this->logger->log(LogLevel::DEBUG, "doesn't fit on layer even when empty");
112 2
                    $this->items->extract();
113 2
                    continue;
114
                }
115
116 17
                $remainingWidth = $layerWidth ? min(floor($layerWidth * 1.1), $this->box->getInnerWidth()) : $this->box->getInnerWidth();
117 17
                $remainingLength = $layerLength ? min(floor($layerLength * 1.1), $this->box->getInnerLength()) : $this->box->getInnerLength();
118 17
                $remainingDepth -= $layerDepth;
119
120 17
                $layerWidth = $layerLength = $layerDepth = 0;
121 17
                $this->logger->log(LogLevel::DEBUG, "doesn't fit, so starting next vertical layer");
122
            }
123
        }
124 23
        $this->logger->log(LogLevel::DEBUG, "done with this box");
125 23
        return new PackedBox($this->box, $packedItems, $remainingWidth, $remainingLength, $remainingDepth, $remainingWeight);
126
    }
127
128
    /**
129
     * @param Item $item
130
     * @param int $remainingDepth
131
     * @param int $remainingWeight
132
     * @return bool
133
     */
134 23
    protected function isItemTooLargeForBox(Item $item, $remainingDepth, $remainingWeight) {
135 23
        return $item->getDepth() > $remainingDepth || $item->getWeight() > $remainingWeight;
136
    }
137
138
    /**
139
     * Figure out space left for next item if we pack this one in it's regular orientation
140
     * @param Item $item
141
     * @param int $remainingWidth
142
     * @param int $remainingLength
143
     * @return int
144
     */
145 23
    protected function fitsSameGap(Item $item, $remainingWidth, $remainingLength) {
146 23
        return min($remainingWidth - $item->getWidth(), $remainingLength - $item->getLength());
147
    }
148
149
    /**
150
     * Figure out space left for next item if we pack this one rotated by 90deg
151
     * @param Item $item
152
     * @param int $remainingWidth
153
     * @param int $remainingLength
154
     * @return int
155
     */
156 23
    protected function fitsRotatedGap(Item $item, $remainingWidth, $remainingLength) {
157 23
        return min($remainingWidth - $item->getLength(), $remainingLength - $item->getWidth());
158
    }
159
160
    /**
161
     * @param Item $item
162
     * @param Item|null $nextItem
163
     * @param $remainingWidth
164
     * @param $remainingLength
165
     * @return bool
166
     */
167 23
    protected function fitsBetterRotated(Item $item, Item $nextItem = null, $remainingWidth, $remainingLength) {
168
169 23
        $fitsSameGap = $this->fitsSameGap($item, $remainingWidth, $remainingLength);
170 23
        $fitsRotatedGap = $this->fitsRotatedGap($item, $remainingWidth, $remainingLength);
171
172 23
        return !!($fitsRotatedGap < 0 ||
173 22
        ($fitsSameGap >= 0 && $fitsSameGap <= $fitsRotatedGap) ||
174 23
        ($item->getWidth() <= $remainingWidth && $nextItem == $item && $remainingLength >= 2 * $item->getLength()));
175
    }
176
177
    /**
178
     * Does item fit in specified gap
179
     * @param Item $item
180
     * @param $remainingWidth
181
     * @param $remainingLength
182
     * @return bool
183
     */
184 23
    protected function fitsGap(Item $item, $remainingWidth, $remainingLength) {
185 23
        return $this->fitsSameGap($item, $remainingWidth, $remainingLength) >= 0 ||
186 23
               $this->fitsRotatedGap($item, $remainingWidth, $remainingLength) >= 0;
187
    }
188
189
    /**
190
     * Figure out if we can stack the next item vertically on top of this rather than side by side
191
     * Used when we've packed a tall item, and have just put a shorter one next to it
192
     * @param Item $item
193
     * @param Item $nextItem
194
     * @param $maxStackDepth
195
     * @param $remainingWeight
196
     * @return bool
197
     */
198 22
    protected function canStackItemInLayer(Item $item, Item $nextItem, $maxStackDepth, $remainingWeight)
199
    {
200 22
        return $nextItem->getDepth() <= $maxStackDepth &&
201 22
               $nextItem->getWeight() <= $remainingWeight &&
202 22
               $nextItem->getWidth() <= $item->getWidth() &&
203 22
               $nextItem->getLength() <= $item->getLength();
204
    }
205
206
    /**
207
     * @param $layerWidth
208
     * @param $layerLength
209
     * @param $layerDepth
210
     * @return bool
211
     */
212 19
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth) {
213 19
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
214
    }
215
}
216