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

WeightRedistributor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
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 WeightRedistributor implements LoggerAwareInterface
20
{
21
22
    use LoggerAwareTrait;
23
24
    /**
25
     * List of box sizes available to pack items into
26
     * @var BoxList
27
     */
28
    protected $boxes;
29
30
    /**
31
     * Constructor
32
     *
33
     * @param BoxList $boxList
34
     */
35 5
    public function __construct(BoxList $boxList)
36
    {
37 5
        $this->boxes = clone $boxList;
38 5
        $this->logger = new NullLogger();
39
    }
40
41
    /**
42
     * Given a solution set of packed boxes, repack them to achieve optimum weight distribution
43
     *
44
     * @param PackedBoxList $originalBoxes
45
     * @return PackedBoxList
46
     */
47 5
    public function redistributeWeight(PackedBoxList $originalBoxes)
48
    {
49
50 5
        $targetWeight = $originalBoxes->getMeanWeight();
51 5
        $this->logger->log(LogLevel::DEBUG, "repacking for weight distribution, weight variance {$originalBoxes->getWeightVariance()}, target weight {$targetWeight}");
52
53 5
        $packedBoxes = new PackedBoxList;
54
55 5
        $overWeightBoxes = [];
56 5
        $underWeightBoxes = [];
57 5
        foreach (clone $originalBoxes as $packedBox) {
58 5
            $boxWeight = $packedBox->getWeight();
59 5
            if ($boxWeight > $targetWeight) {
60 3
                $overWeightBoxes[] = $packedBox;
61 5
            } elseif ($boxWeight < $targetWeight) {
62 3
                $underWeightBoxes[] = $packedBox;
63
            } else {
64 5
                $packedBoxes->insert($packedBox); //target weight, so we'll keep these
65
            }
66
        }
67
68
        do { //Keep moving items from most overweight box to most underweight box
69 5
            $tryRepack = false;
70 5
            $this->logger->log(LogLevel::DEBUG, 'boxes under/over target: ' . count($underWeightBoxes) . '/' . count($overWeightBoxes));
71
72 5
            foreach ($underWeightBoxes as $u => $underWeightBox) {
73 3
                $this->logger->log(LogLevel::DEBUG, 'Underweight Box ' . $u);
74 3
                foreach ($overWeightBoxes as $o => $overWeightBox) {
75 3
                    $this->logger->log(LogLevel::DEBUG, 'Overweight Box ' . $o);
76 3
                    $overWeightBoxItems = $overWeightBox->getItems()->asArray();
77
78
                    //For each item in the heavier box, try and move it to the lighter one
79
                    /** @var Item $overWeightBoxItem */
80 3
                    foreach ($overWeightBoxItems as $oi => $overWeightBoxItem) {
81 3
                        $this->logger->log(LogLevel::DEBUG, 'Overweight Item ' . $oi);
82 3
                        if ($underWeightBox->getWeight() + $overWeightBoxItem->getWeight() > $targetWeight) {
83 3
                            $this->logger->log(LogLevel::DEBUG, 'Skipping item for hindering weight distribution');
84 3
                            continue; //skip if moving this item would hinder rather than help weight distribution
85
                        }
86
87 1
                        $newItemsForLighterBox = $underWeightBox->getItems()->asArray();
88 1
                        $newItemsForLighterBox[] = $overWeightBoxItem;
89
90 1
                        $newLighterBoxPacker = new Packer(); //we may need a bigger box
91 1
                        $newLighterBoxPacker->setBoxes($this->boxes);
92 1
                        $newLighterBoxPacker->setItems($newItemsForLighterBox);
93 1
                        $this->logger->log(LogLevel::INFO, "[ATTEMPTING TO PACK LIGHTER BOX]");
94 1
                        $newLighterBox = $newLighterBoxPacker->doVolumePacking()->extract();
95
96 1
                        if ($newLighterBox->getItems()->count() === count($newItemsForLighterBox)) { //new item fits
97 1
                            $this->logger->log(LogLevel::DEBUG, 'New item fits');
98 1
                            unset($overWeightBoxItems[$oi]); //now packed in different box
99
100 1
                            if (count($overWeightBoxItems) > 0) {
101 1
                                $newHeavierBoxPacker = new Packer(); //we may be able to use a smaller box
102 1
                                $newHeavierBoxPacker->setBoxes($this->boxes);
103 1
                                $newHeavierBoxPacker->setItems($overWeightBoxItems);
104
105 1
                                $this->logger->log(LogLevel::INFO, "[ATTEMPTING TO PACK HEAVIER BOX]");
106 1
                                $newHeavierBoxes = $newHeavierBoxPacker->doVolumePacking();
107 1
                                if ($newHeavierBoxes->count()
108 1
                                    > 1) { //found an edge case in packing algorithm that *increased* box count
109
                                    $this->logger->log(
110
                                        LogLevel::INFO,
111
                                        "[REDISTRIBUTING WEIGHT] Abandoning redistribution, because new packing is less efficient than original"
112
                                    );
113
114
                                    return $originalBoxes;
115
                                }
116
117 1
                                $overWeightBoxes[$o] = $newHeavierBoxes->extract();
118
                            } else {
119
                                unset($overWeightBoxes[$o]);
120
                            }
121 1
                            $underWeightBoxes[$u] = $newLighterBox;
122
123 1
                            $tryRepack = true; //we did some work, so see if we can do even better
124 1
                            usort($overWeightBoxes, [$packedBoxes, 'reverseCompare']);
125 1
                            usort($underWeightBoxes, [$packedBoxes, 'reverseCompare']);
126 3
                            break 3;
127
                        }
128
                    }
129
                }
130
            }
131 5
        } while ($tryRepack);
132
133
        //Combine back into a single list
134 5
        $packedBoxes->insertFromArray($overWeightBoxes);
135 5
        $packedBoxes->insertFromArray($underWeightBoxes);
136
137 5
        return $packedBoxes;
138
    }
139
}
140