Completed
Push — 1.x-dev ( e51b9c )
by Doug
09:00
created

OrientatedItemFactory::getBestOrientation()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 14
cts 14
cp 1
rs 9.2
c 0
b 0
f 0
cc 3
eloc 15
nc 4
nop 7
crap 3
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
12
/**
13
 * Figure out orientations for an item and a given set of dimensions
14
 * @author Doug Wright
15
 * @package BoxPacker
16
 */
17
class OrientatedItemFactory implements LoggerAwareInterface
18
{
19
    use LoggerAwareTrait;
20
21
    /**
22
     * @var OrientatedItem[]
23
     */
24
    protected static $emptyBoxCache = [];
25
26
    /**
27
     * Get the best orientation for an item
28
     * @param Box $box
29
     * @param Item $item
30
     * @param PackedItem|null $prevItem
31
     * @param bool $isLastItem
32
     * @param int $widthLeft
33
     * @param int $lengthLeft
34
     * @param int $depthLeft
35
     * @return OrientatedItem|false
36
     */
37 33
    public function getBestOrientation(Box $box, Item $item, PackedItem $prevItem = null, $isLastItem, $widthLeft, $lengthLeft, $depthLeft) {
38
39 33
        $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft);
40 33
        $usableOrientations = $this->getUsableOrientations($possibleOrientations, $box, $item, $isLastItem);
41
42 33
        $orientationFits = [];
43
        /** @var OrientatedItem $orientation */
44 33
        foreach ($usableOrientations as $o => $orientation) {
45 33
            $orientationFit = min($widthLeft - $orientation->getWidth(), $lengthLeft - $orientation->getLength());
46 33
            $orientationFits[$o] = $orientationFit;
47
        }
48
49 33
        if (!empty($orientationFits)) {
50 33
            asort($orientationFits);
51 33
            reset($orientationFits);
52 33
            $bestFit = $usableOrientations[key($orientationFits)];
53 33
            $this->logger->debug("Selected best fit orientation", ['orientation' => $bestFit]);
54 33
            return $bestFit;
55
        } else {
56 32
            return false;
57
        }
58
    }
59
60
    /**
61
     * Find all possible orientations for an item
62
     * @param Item $item
63
     * @param PackedItem|null $prevItem
64
     * @param int $widthLeft
65
     * @param int $lengthLeft
66
     * @param int $depthLeft
67
     * @return OrientatedItem[]
68
     */
69 33
    public function getPossibleOrientations(Item $item, PackedItem $prevItem = null, $widthLeft, $lengthLeft, $depthLeft) {
70
71 33
        $orientations = [];
72
73
        //Special case items that are the same as what we just packed - keep orientation
74
        /** @noinspection PhpNonStrictObjectEqualityInspection */
75 33
        if ($prevItem && $prevItem->getItem() == $item) {
76 13
            $orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
77
        } else {
78
79
            //simple 2D rotation
80 33
            $orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
81 33
            $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
82
        }
83
84
        //remove any that simply don't fit
85 33
        return array_filter($orientations, function(OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
86 33
            return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
87 33
        });
88
    }
89
90
    /**
91
     * @param Item $item
92
     * @param Box  $box
93
     * @return OrientatedItem[]
0 ignored issues
show
Documentation introduced by
Should the return type not be OrientatedItem|OrientatedItem[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
94
     */
95 33
    public function getPossibleOrientationsInEmptyBox(Item $item, Box $box)
96
    {
97 33
        $cacheKey = $item->getWidth() .
98 33
            '|' .
99 33
            $item->getLength() .
100 33
            '|' .
101 33
            $item->getDepth() .
102 33
            '|' .
103 33
            $box->getInnerWidth() .
104 33
            '|' .
105 33
            $box->getInnerLength() .
106 33
            '|' .
107 33
            $box->getInnerDepth();
108
109 33
        if (isset(static::$emptyBoxCache[$cacheKey])) {
110 28
            $orientations = static::$emptyBoxCache[$cacheKey];
111
        } else {
112 30
            $orientations = $this->getPossibleOrientations(
113 30
                $item,
114 30
                null,
115 30
                $box->getInnerWidth(),
116 30
                $box->getInnerLength(),
117 30
                $box->getInnerDepth()
118
            );
119 30
            static::$emptyBoxCache[$cacheKey] = $orientations;
120
        }
121 33
        return $orientations;
122
    }
123
124
    /**
125
     * @param OrientatedItem[] $possibleOrientations
126
     * @param Box              $box
127
     * @param Item             $item
128
     * @param bool             $isLastItem
129
     *
130
     * @return OrientatedItem[]
131
     */
132 33
    protected function getUsableOrientations(
133
        $possibleOrientations,
134
        Box $box,
135
        Item $item,
136
        $isLastItem
137
    ) {
138
        /*
139
         * Divide possible orientations into stable (low centre of gravity) and unstable (high centre of gravity)
140
         */
141 33
        $stableOrientations = [];
142 33
        $unstableOrientations = [];
143
144 33
        foreach ($possibleOrientations as $o => $orientation) {
145 33
            if ($orientation->isStable()) {
146 31
                $stableOrientations[] = $orientation;
147
            } else {
148 33
                $unstableOrientations[] = $orientation;
149
            }
150
        }
151
152 33
        $orientationsToUse = [];
153
154
        /*
155
         * We prefer to use stable orientations only, but allow unstable ones if either
156
         * the item is the last one left to pack OR
157
         * the item doesn't fit in the box any other way
158
         */
159 33
        if (count($stableOrientations) > 0) {
160 31
            $orientationsToUse = $stableOrientations;
161 32
        } else if (count($unstableOrientations) > 0) {
162 3
            $orientationsInEmptyBox = $this->getPossibleOrientationsInEmptyBox($item, $box);
163
164 3
            $stableOrientationsInEmptyBox = array_filter(
165 3
                $orientationsInEmptyBox,
166 3
                function(OrientatedItem $orientation) {
167 3
                    return $orientation->isStable();
168 3
                }
169
            );
170
171
            if ($isLastItem || count($stableOrientationsInEmptyBox) == 0) {
172
                $orientationsToUse = $unstableOrientations;
173
            }
174
        }
175
176
        return $orientationsToUse;
177
    }
178
}
179
180