Completed
Push — factor_out_orientations ( 5fabe5...6c5e91 )
by Doug
05:06
created

VolumePacker::tryAndStackItemsIntoSpace()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 30
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 12
cts 12
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 24
nc 3
nop 6
crap 4
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\NullLogger;
12
13
/**
14
 * Actual packer
15
 * @author Doug Wright
16
 * @package BoxPacker
17
 */
18
class VolumePacker implements LoggerAwareInterface
19
{
20
    use LoggerAwareTrait;
21
22
    /**
23
     * Box to pack items into
24
     * @var Box
25
     */
26
    protected $box;
27
28
    /**
29
     * List of items to be packed
30
     * @var ItemList
31
     */
32
    protected $items;
33
34
    /**
35
     * Remaining width of the box to pack items into
36
     * @var int
37
     */
38
    protected $widthLeft;
39
40
    /**
41
     * Remaining length of the box to pack items into
42
     * @var int
43
     */
44
    protected $lengthLeft;
45
46
    /**
47
     * Remaining depth of the box to pack items into
48
     * @var int
49
     */
50
    protected $depthLeft;
51
52
    /**
53
     * Remaining weight capacity of the box
54
     * @var int
55
     */
56
    protected $remainingWeight;
57
58
    /**
59
     * Used width inside box for packing items
60
     * @var int
61
     */
62
    protected $usedWidth = 0;
63
64
    /**
65
     * Used length inside box for packing items
66
     * @var int
67
     */
68
    protected $usedLength = 0;
69
70
    /**
71
     * Used depth inside box for packing items
72
     * @var int
73
     */
74
    protected $usedDepth = 0;
75
76
    /**
77
     * Constructor
78
     */
79 30
    public function __construct(Box $box, ItemList $items)
80
    {
81 30
        $this->logger = new NullLogger();
82
83 30
        $this->box = $box;
84 30
        $this->items = $items;
85
86 30
        $this->depthLeft = $this->box->getInnerDepth();
87 30
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
88 30
        $this->widthLeft = $this->box->getInnerWidth();
89 30
        $this->lengthLeft = $this->box->getInnerLength();
90
    }
91
92
    /**
93
     * Pack as many items as possible into specific given box
94
     * @return PackedBox packed box
95
     */
96 30
    public function pack()
97
    {
98 30
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
99
100 30
        $packedItems = new ItemList;
101
102 30
        $layerWidth = $layerLength = $layerDepth = 0;
103
104 30
        $prevItem = null;
105
106 30
        while (!$this->items->isEmpty()) {
107
108 30
            $itemToPack = $this->items->extract();
109
110
            //skip items that are simply too heavy
111 30
            if ($itemToPack->getWeight() > $this->remainingWeight) {
112 4
                continue;
113
            }
114
115 30
            $this->logger->debug(
116 30
                "evaluating item {$itemToPack->getDescription()}",
117
                [
118 30
                    'item' => $itemToPack,
119
                    'space' => [
120 30
                        'widthLeft'   => $this->widthLeft,
121 30
                        'lengthLeft'  => $this->lengthLeft,
122 30
                        'depthLeft'   => $this->depthLeft,
123 30
                        'layerWidth'  => $layerWidth,
124 30
                        'layerLength' => $layerLength,
125 30
                        'layerDepth'  => $layerDepth
126
                    ]
127
                ]
128
            );
129
130 30
            $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
131
132 30
            $orientatedItemFactory = new OrientatedItemFactory();
133 30
            $orientatedItemFactory->setLogger($this->logger);
134 30
            $orientatedItem = $orientatedItemFactory->getBestOrientation($this->box, $itemToPack, $prevItem, $nextItem, $this->widthLeft, $this->lengthLeft, $this->depthLeft);
135
136 30
            if ($orientatedItem) {
137
138 30
                $packedItems->insert($orientatedItem->getItem());
139 30
                $this->remainingWeight -= $itemToPack->getWeight();
140
141 30
                $this->lengthLeft -= $orientatedItem->getLength();
142 30
                $layerLength += $orientatedItem->getLength();
143 30
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
144
145 30
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
146
147 30
                $this->usedLength = max($this->usedLength, $layerLength);
148 30
                $this->usedWidth = max($this->usedWidth, $layerWidth);
149
150
                //allow items to be stacked in place within the same footprint up to current layerdepth
151 30
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
152 30
                $this->tryAndStackItemsIntoSpace($packedItems, $prevItem, $nextItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth);
153
154 30
                $prevItem = $orientatedItem;
155
156 30
                if (!$nextItem) {
157 30
                    $this->usedDepth += $layerDepth;
158
                }
159
            } else {
160
161 24
                $prevItem = null;
162
163 24
                if ($this->widthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
164 23
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
165 23
                    $this->lengthLeft += $layerLength;
166 23
                    $this->widthLeft -= $layerWidth;
167 23
                    $layerWidth = $layerLength = 0;
168 23
                    $this->items->insert($itemToPack);
169 23
                    continue;
170 18
                } elseif ($this->lengthLeft < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
171 7
                    $this->logger->debug("doesn't fit on layer even when empty");
172 7
                    continue;
173
                }
174
175 17
                $this->widthLeft = $layerWidth ? min(floor($layerWidth * 1.1), $this->box->getInnerWidth()) : $this->box->getInnerWidth();
0 ignored issues
show
Documentation Bug introduced by
It seems like $layerWidth ? min(floor(...s->box->getInnerWidth() can also be of type double. However, the property $widthLeft is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
176 17
                $this->lengthLeft = $layerLength ? min(floor($layerLength * 1.1), $this->box->getInnerLength()) : $this->box->getInnerLength();
0 ignored issues
show
Documentation Bug introduced by
It seems like $layerLength ? min(floor...->box->getInnerLength() can also be of type double. However, the property $lengthLeft is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
177 17
                $this->depthLeft -= $layerDepth;
178 17
                $this->usedDepth += $layerDepth;
179
180 17
                $layerWidth = $layerLength = $layerDepth = 0;
181 17
                $this->logger->debug("doesn't fit, so starting next vertical layer");
182 17
                $this->items->insert($itemToPack);
183
            }
184
        }
185 30
        $this->logger->debug("done with this box");
186 30
        return new PackedBox(
187 30
            $this->box,
188
            $packedItems,
189 30
            $this->widthLeft,
190 30
            $this->lengthLeft,
191 30
            $this->depthLeft,
192 30
            $this->remainingWeight,
193 30
            $this->usedWidth,
194 30
            $this->usedLength,
195 30
            $this->usedDepth);
196
    }
197
198
    /**
199
     * Figure out if we can stack the next item vertically on top of this rather than side by side
200
     * Used when we've packed a tall item, and have just put a shorter one next to it
201
     *
202
     * @param ItemList       $packedItems
203
     * @param OrientatedItem $prevItem
0 ignored issues
show
Documentation introduced by
Should the type for parameter $prevItem not be null|OrientatedItem?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
204
     * @param Item           $nextItem
0 ignored issues
show
Documentation introduced by
Should the type for parameter $nextItem not be null|Item?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
205
     * @param int            $maxWidth
206
     * @param int            $maxLength
207
     * @param int            $maxDepth
208
     */
209 30
    protected function tryAndStackItemsIntoSpace(
210
        ItemList $packedItems,
211
        OrientatedItem $prevItem = null,
212
        Item $nextItem = null,
213
        $maxWidth,
214
        $maxLength,
215
        $maxDepth
216
    ) {
217 30
        $orientatedItemFactory = new OrientatedItemFactory();
218 30
        $orientatedItemFactory->setLogger($this->logger);
219
220 30
        while (!$this->items->isEmpty() && $this->remainingWeight >= $this->items->top()->getWeight()) {
221 28
            $stackedItem = $orientatedItemFactory->getBestOrientation(
222 28
                $this->box,
223 28
                $this->items->top(),
224
                $prevItem,
225
                $nextItem,
226
                $maxWidth,
227
                $maxLength,
228
                $maxDepth
229
            );
230 28
            if ($stackedItem) {
231 2
                $this->remainingWeight -= $this->items->top()->getWeight();
232 2
                $maxDepth -= $stackedItem->getDepth();
233 2
                $packedItems->insert($this->items->extract());
234
            } else {
235 28
                break;
236
            }
237
        }
238
    }
239
240
    /**
241
     * @param int $layerWidth
242
     * @param int $layerLength
243
     * @param int $layerDepth
244
     * @return bool
245
     */
246 24
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth)
247
    {
248 24
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
249
    }
250
}
251