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

VolumePacker.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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->extract();
63
64
            //skip items that are simply too heavy
65 23
            if ($itemToPack->getWeight() > $remainingWeight) {
66 8
                continue;
67 8
            }
68
69
            $this->logger->debug("evaluating item {$itemToPack->getDescription()}");
70 23
            $this->logger->debug("remaining width: {$remainingWidth}, length: {$remainingLength}, depth: {$remainingDepth}");
71 23
            $this->logger->debug("layerWidth: {$layerWidth}, layerLength: {$layerLength}, layerDepth: {$layerDepth}");
72 23
73
            $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
74 23
            $orientatedItem = $this->findBestOrientation($itemToPack, $nextItem, $remainingWidth, $remainingLength, $remainingDepth);
75 23
76
            if ($orientatedItem) {
77 23
78
                $packedItems->insert($orientatedItem->getItem());
79 23
                $remainingWeight -= $itemToPack->getWeight();
80 23
81
                $remainingLength -= $orientatedItem->getLength();
82 23
                $layerLength += $orientatedItem->getLength();
83 23
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
84 22
85 22
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
86 22
87 22
                //allow items to be stacked in place within the same footprint up to current layerdepth
88 22
                $maxStackDepth = $layerDepth - $orientatedItem->getDepth();
89 8
                while (!$this->items->isEmpty() && $this->canStackItemInLayer($itemToPack, $this->items->top(), $maxStackDepth, $remainingWeight)) {
90 8
                    $remainingWeight -= $this->items->top()->getWeight();
91 8
                    $maxStackDepth -= $this->items->top()->getDepth(); // XXX no attempt at best fit
92 8
                    $packedItems->insert($this->items->extract());
93
                }
94 23
            } else {
95
                if ($remainingWidth >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
96
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
97 23
                    $remainingLength += $layerLength;
98 23
                    $remainingWidth -= $layerWidth;
99 1
                    $layerWidth = $layerLength = 0;
100 1
                    $this->items->insert($itemToPack);
101 1
                    continue;
102 1
                } elseif ($remainingLength < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
103 23
                    $this->logger->debug("doesn't fit on layer even when empty");
104 19
                    continue;
105 19
                }
106 19
107 19
                $remainingWidth = $layerWidth ? min(floor($layerWidth * 1.1), $this->box->getInnerWidth()) : $this->box->getInnerWidth();
108 19
                $remainingLength = $layerLength ? min(floor($layerLength * 1.1), $this->box->getInnerLength()) : $this->box->getInnerLength();
109 19
                $remainingDepth -= $layerDepth;
110 17
111 2
                $layerWidth = $layerLength = $layerDepth = 0;
112 2
                $this->logger->log(LogLevel::DEBUG, "doesn't fit, so starting next vertical layer");
113 2
                $this->items->insert($itemToPack);
114
            }
115
        }
116 17
        $this->logger->log(LogLevel::DEBUG, "done with this box");
117 17
        return new PackedBox($this->box, $packedItems, $remainingWidth, $remainingLength, $remainingDepth, $remainingWeight);
118 17
    }
119
120 17
    /**
121 17
     * Figure out space left for next item if we pack this one in it's regular orientation
122
     * @param Item $item
123 23
     * @param int $remainingWidth
124 23
     * @param int $remainingLength
125 23
     * @return int
126
     */
127
    protected function fitsSameGap(Item $item, $remainingWidth, $remainingLength) {
128
        return min($remainingWidth - $item->getWidth(), $remainingLength - $item->getLength());
129
    }
130
131
    /**
132
     * Figure out space left for next item if we pack this one rotated by 90deg
133
     * @param Item $item
134 23
     * @param int $remainingWidth
135 23
     * @param int $remainingLength
136
     * @return int
137
     */
138
    protected function fitsRotatedGap(Item $item, $remainingWidth, $remainingLength) {
139
        return min($remainingWidth - $item->getLength(), $remainingLength - $item->getWidth());
140
    }
141
142
    /**
143
     * Get the best orientation for an item
144
     * @param Item $item
145 23
     * @param Item|null $nextItem
146 23
     * @param int $remainingWidth
147
     * @param int $remainingLength
148
     * @param int $remainingDepth
149
     * @return bool|OrientatedItem
0 ignored issues
show
Consider making the return type a bit more specific; maybe use false|OrientatedItem.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
150
     */
151
    protected function findBestOrientation(Item $item, Item $nextItem = null, $remainingWidth, $remainingLength, $remainingDepth) {
152
153
        $fitsSameGap = $this->fitsSameGap($item, $remainingWidth, $remainingLength);
154
        $fitsRotatedGap = $this->fitsRotatedGap($item, $remainingWidth, $remainingLength);
155
        $fitsDepth = $item->getDepth() <= $remainingDepth;
156 23
157 23
        $fitsAtAll = $fitsDepth && ($fitsSameGap >= 0 || $fitsRotatedGap >= 0);
158
159
        if (!$fitsAtAll) {
160
            return false;
161
        }
162
163
        $betterUnRotated = !!($fitsRotatedGap < 0 ||
164
            ($fitsSameGap >= 0 && $fitsSameGap <= $fitsRotatedGap) ||
165
            ($item->getWidth() <= $remainingWidth && $nextItem == $item && $remainingLength >= 2 * $item->getLength()));
166
167 23
        if ($betterUnRotated) {
168
            $this->logger->debug("fits (better) unrotated");
169 23
            return new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
170 23
        } else {
171
            $this->logger->debug("fits (better) rotated");
172 23
            return new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
173 22
        }
174 23
    }
175
176
    /**
177
     * @param Item $item
178
     * @param Item|null $nextItem
179
     * @param $remainingWidth
180
     * @param $remainingLength
181
     * @return bool
182
     */
183
    protected function fitsBetterUnrotated(Item $item, Item $nextItem = null, $remainingWidth, $remainingLength) {
184 23
185 23
        $fitsSameGap = $this->fitsSameGap($item, $remainingWidth, $remainingLength);
186 23
        $fitsRotatedGap = $this->fitsRotatedGap($item, $remainingWidth, $remainingLength);
187
188
        return !!($fitsRotatedGap < 0 ||
189
        ($fitsSameGap >= 0 && $fitsSameGap <= $fitsRotatedGap) ||
190
        ($item->getWidth() <= $remainingWidth && $nextItem == $item && $remainingLength >= 2 * $item->getLength()));
191
    }
192
193
    /**
194
     * Figure out if we can stack the next item vertically on top of this rather than side by side
195
     * Used when we've packed a tall item, and have just put a shorter one next to it
196
     * @param Item $item
197
     * @param Item $nextItem
198 22
     * @param $maxStackDepth
199
     * @param $remainingWeight
200 22
     * @return bool
201 22
     */
202 22
    protected function canStackItemInLayer(Item $item, Item $nextItem, $maxStackDepth, $remainingWeight)
203 22
    {
204
        return $nextItem->getDepth() <= $maxStackDepth &&
205
               $nextItem->getWeight() <= $remainingWeight &&
206
               $nextItem->getWidth() <= $item->getWidth() &&
207
               $nextItem->getLength() <= $item->getLength();
208
    }
209
210
    /**
211
     * @param $layerWidth
212 19
     * @param $layerLength
213 19
     * @param $layerDepth
214
     * @return bool
215
     */
216
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth) {
217
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
218
    }
219
}
220