Completed
Push — 2.x-dev ( 699574...0cd62f )
by Doug
05:24
created

WeightRedistributor   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 118
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 91.8%

Importance

Changes 0
Metric Value
wmc 13
lcom 1
cbo 5
dl 0
loc 118
ccs 56
cts 61
cp 0.918
rs 10
c 0
b 0
f 0

2 Methods

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