Completed
Push — 2.x-dev ( 5fabe5...97c472 )
by Doug
06:03
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
     * @param Box      $box
80
     * @param ItemList $items
81
     */
82 31
    public function __construct(Box $box, ItemList $items)
83
    {
84 31
        $this->logger = new NullLogger();
85
86 31
        $this->box = $box;
87 31
        $this->items = $items;
88
89 31
        $this->depthLeft = $this->box->getInnerDepth();
90 31
        $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
91 31
        $this->widthLeft = $this->box->getInnerWidth();
92 31
        $this->lengthLeft = $this->box->getInnerLength();
93
    }
94
95
    /**
96
     * Pack as many items as possible into specific given box
97
     * @return PackedBox packed box
98
     */
99 31
    public function pack()
100
    {
101 31
        $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}");
102
103 31
        $packedItems = new ItemList;
104
105 31
        $layerWidth = $layerLength = $layerDepth = 0;
106
107 31
        $prevItem = null;
108
109 31
        while (!$this->items->isEmpty()) {
110
111 31
            $itemToPack = $this->items->extract();
112
113
            //skip items that are simply too heavy
114 31
            if (!$this->checkNonDimensionalConstraints($itemToPack, $packedItems)) {
115 5
                continue;
116
            }
117
118 31
            $this->logger->debug(
119 31
                "evaluating item {$itemToPack->getDescription()} for fit",
120
                [
121 31
                    'item' => $itemToPack,
122
                    'space' => [
123 31
                        'widthLeft'   => $this->widthLeft,
124 31
                        'lengthLeft'  => $this->lengthLeft,
125 31
                        'depthLeft'   => $this->depthLeft,
126 31
                        'layerWidth'  => $layerWidth,
127 31
                        'layerLength' => $layerLength,
128 31
                        'layerDepth'  => $layerDepth
129
                    ]
130
                ]
131
            );
132
133 31
            $nextItem = !$this->items->isEmpty() ? $this->items->top() : null;
134
135 31
            $orientatedItemFactory = new OrientatedItemFactory();
136 31
            $orientatedItemFactory->setLogger($this->logger);
137 31
            $orientatedItem = $orientatedItemFactory->getBestOrientation($this->box, $itemToPack, $prevItem, $nextItem, $this->widthLeft, $this->lengthLeft, $this->depthLeft);
138
139 31
            if ($orientatedItem) {
140
141 31
                $packedItems->insert($orientatedItem->getItem());
142 31
                $this->remainingWeight -= $itemToPack->getWeight();
143
144 31
                $this->lengthLeft -= $orientatedItem->getLength();
145 31
                $layerLength += $orientatedItem->getLength();
146 31
                $layerWidth = max($orientatedItem->getWidth(), $layerWidth);
147
148 31
                $layerDepth = max($layerDepth, $orientatedItem->getDepth()); //greater than 0, items will always be less deep
149
150 31
                $this->usedLength = max($this->usedLength, $layerLength);
151 31
                $this->usedWidth = max($this->usedWidth, $layerWidth);
152
153
                //allow items to be stacked in place within the same footprint up to current layerdepth
154 31
                $stackableDepth = $layerDepth - $orientatedItem->getDepth();
155 31
                $this->tryAndStackItemsIntoSpace($packedItems, $prevItem, $nextItem, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth);
156
157 31
                $prevItem = $orientatedItem;
158
159 31
                if (!$nextItem) {
160 31
                    $this->usedDepth += $layerDepth;
161
                }
162
            } else {
163
164 24
                $prevItem = null;
165
166 24
                if ($this->widthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength()) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) {
167 23
                    $this->logger->debug("No more fit in lengthwise, resetting for new row");
168 23
                    $this->lengthLeft += $layerLength;
169 23
                    $this->widthLeft -= $layerWidth;
170 23
                    $layerWidth = $layerLength = 0;
171 23
                    $this->items->insert($itemToPack);
172 23
                    continue;
173 18
                } elseif ($this->lengthLeft < min($itemToPack->getWidth(), $itemToPack->getLength()) || $layerDepth == 0) {
174 7
                    $this->logger->debug("doesn't fit on layer even when empty");
175 7
                    continue;
176
                }
177
178 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...
179 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...
180 17
                $this->depthLeft -= $layerDepth;
181 17
                $this->usedDepth += $layerDepth;
182
183 17
                $layerWidth = $layerLength = $layerDepth = 0;
184 17
                $this->logger->debug("doesn't fit, so starting next vertical layer");
185 17
                $this->items->insert($itemToPack);
186
            }
187
        }
188 31
        $this->logger->debug("done with this box");
189 31
        return new PackedBox(
190 31
            $this->box,
191
            $packedItems,
192 31
            $this->widthLeft,
193 31
            $this->lengthLeft,
194 31
            $this->depthLeft,
195 31
            $this->remainingWeight,
196 31
            $this->usedWidth,
197 31
            $this->usedLength,
198 31
            $this->usedDepth);
199
    }
200
201
    /**
202
     * Figure out if we can stack the next item vertically on top of this rather than side by side
203
     * Used when we've packed a tall item, and have just put a shorter one next to it
204
     *
205
     * @param ItemList       $packedItems
206
     * @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...
207
     * @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...
208
     * @param int            $maxWidth
209
     * @param int            $maxLength
210
     * @param int            $maxDepth
211
     */
212 31
    protected function tryAndStackItemsIntoSpace(
213
        ItemList $packedItems,
214
        OrientatedItem $prevItem = null,
215
        Item $nextItem = null,
216
        $maxWidth,
217
        $maxLength,
218
        $maxDepth
219
    ) {
220 31
        $orientatedItemFactory = new OrientatedItemFactory();
221 31
        $orientatedItemFactory->setLogger($this->logger);
222
223 31
        while (!$this->items->isEmpty() && $this->remainingWeight >= $this->items->top()->getWeight()) {
224 29
            $stackedItem = $orientatedItemFactory->getBestOrientation(
225 29
                $this->box,
226 29
                $this->items->top(),
227
                $prevItem,
228
                $nextItem,
229
                $maxWidth,
230
                $maxLength,
231
                $maxDepth
232
            );
233 29
            if ($stackedItem) {
234 2
                $this->remainingWeight -= $this->items->top()->getWeight();
235 2
                $maxDepth -= $stackedItem->getDepth();
236 2
                $packedItems->insert($this->items->extract());
237
            } else {
238 29
                break;
239
            }
240
        }
241
    }
242
243
    /**
244
     * @param int $layerWidth
245
     * @param int $layerLength
246
     * @param int $layerDepth
247
     * @return bool
248
     */
249 24
    protected function isLayerStarted($layerWidth, $layerLength, $layerDepth)
250
    {
251 24
        return $layerWidth > 0 && $layerLength > 0 && $layerDepth > 0;
252
    }
253
254
    /**
255
     * As well as purely dimensional constraints, there are other constraints that need to be met
256
     * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box)
257
     *
258
     * @param Item     $itemToPack
259
     * @param ItemList $packedItems
260
     *
261
     * @return bool
262
     */
263 31
    protected function checkNonDimensionalConstraints(Item $itemToPack, ItemList $packedItems)
264
    {
265 31
        $weightOK = $itemToPack->getWeight() <= $this->remainingWeight;
266
267 31
        if ($itemToPack instanceof ConstrainedItem) {
268 1
            return $weightOK && $itemToPack->canBePackedInBox(clone $packedItems, $this->box);
269
        }
270
271 30
        return $weightOK;
272
    }
273
}
274