| Total Complexity | 44 |
| Total Lines | 426 |
| Duplicated Lines | 0 % |
| Changes | 9 | ||
| Bugs | 1 | Features | 0 |
Complex classes like VolumePacker often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use VolumePacker, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 18 | class VolumePacker implements LoggerAwareInterface |
||
| 19 | { |
||
| 20 | /** |
||
| 21 | * The logger instance. |
||
| 22 | * |
||
| 23 | * @var LoggerInterface |
||
| 24 | */ |
||
| 25 | protected $logger; |
||
| 26 | |||
| 27 | /** |
||
| 28 | * Box to pack items into. |
||
| 29 | * |
||
| 30 | * @var Box |
||
| 31 | */ |
||
| 32 | protected $box; |
||
| 33 | |||
| 34 | /** |
||
| 35 | * @var int |
||
| 36 | */ |
||
| 37 | protected $boxWidth; |
||
| 38 | |||
| 39 | /** |
||
| 40 | * @var int |
||
| 41 | */ |
||
| 42 | protected $boxLength; |
||
| 43 | |||
| 44 | /** |
||
| 45 | * List of items to be packed. |
||
| 46 | * |
||
| 47 | * @var ItemList |
||
| 48 | */ |
||
| 49 | protected $items; |
||
| 50 | |||
| 51 | /** |
||
| 52 | * List of items temporarily skipped to be packed. |
||
| 53 | * |
||
| 54 | * @var array |
||
| 55 | */ |
||
| 56 | protected $skippedItems = []; |
||
| 57 | |||
| 58 | /** |
||
| 59 | * Remaining weight capacity of the box. |
||
| 60 | * |
||
| 61 | * @var int |
||
| 62 | */ |
||
| 63 | protected $remainingWeight; |
||
| 64 | |||
| 65 | /** |
||
| 66 | * Whether the box was rotated for packing. |
||
| 67 | * |
||
| 68 | * @var bool |
||
| 69 | */ |
||
| 70 | protected $boxRotated = false; |
||
| 71 | |||
| 72 | /** |
||
| 73 | * @var PackedLayer[] |
||
| 74 | */ |
||
| 75 | protected $layers = []; |
||
| 76 | |||
| 77 | /** |
||
| 78 | * Whether the packer is in look-ahead mode (i.e. working ahead of the main packing). |
||
| 79 | * |
||
| 80 | * @var bool |
||
| 81 | */ |
||
| 82 | protected $lookAheadMode = false; |
||
| 83 | |||
| 84 | /** |
||
| 85 | * @var OrientatedItemFactory |
||
| 86 | */ |
||
| 87 | private $orientatedItemFactory; |
||
| 88 | |||
| 89 | /** |
||
| 90 | * Constructor. |
||
| 91 | * |
||
| 92 | * @param Box $box |
||
| 93 | * @param ItemList $items |
||
| 94 | */ |
||
| 95 | public function __construct(Box $box, ItemList $items) |
||
| 96 | { |
||
| 97 | $this->box = $box; |
||
| 98 | $this->items = $items; |
||
| 99 | |||
| 100 | $this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength()); |
||
| 101 | $this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength()); |
||
| 102 | $this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight(); |
||
| 103 | $this->logger = new NullLogger(); |
||
| 104 | |||
| 105 | // we may have just rotated the box for packing purposes, record if we did |
||
| 106 | if ($this->box->getInnerWidth() !== $this->boxWidth || $this->box->getInnerLength() !== $this->boxLength) { |
||
| 107 | $this->boxRotated = true; |
||
| 108 | } |
||
| 109 | |||
| 110 | $this->orientatedItemFactory = new OrientatedItemFactory($this->box); |
||
| 111 | } |
||
| 112 | |||
| 113 | /** |
||
| 114 | * Sets a logger. |
||
| 115 | * |
||
| 116 | * @param LoggerInterface $logger |
||
| 117 | */ |
||
| 118 | public function setLogger(LoggerInterface $logger) |
||
| 119 | { |
||
| 120 | $this->logger = $logger; |
||
| 121 | $this->orientatedItemFactory->setLogger($logger); |
||
| 122 | } |
||
| 123 | |||
| 124 | /** |
||
| 125 | * @internal |
||
| 126 | * @param bool $lookAhead |
||
| 127 | */ |
||
| 128 | public function setLookAheadMode($lookAhead) |
||
| 131 | } |
||
| 132 | |||
| 133 | /** |
||
| 134 | * Pack as many items as possible into specific given box. |
||
| 135 | * |
||
| 136 | * @return PackedBox packed box |
||
| 137 | */ |
||
| 138 | public function pack() |
||
| 139 | { |
||
| 140 | $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]); |
||
| 141 | |||
| 142 | while ($this->items->count() > 0) { |
||
| 143 | $layerStartDepth = $this->getCurrentPackedDepth(); |
||
| 144 | $this->packLayer($layerStartDepth, $this->boxWidth, $this->boxLength, $this->box->getInnerDepth() - $layerStartDepth); |
||
| 145 | } |
||
| 146 | |||
| 147 | if ($this->boxRotated) { |
||
| 148 | $this->rotateLayersNinetyDegrees(); |
||
| 149 | } |
||
| 150 | |||
| 151 | if (!$this->lookAheadMode) { |
||
| 152 | $this->stabiliseLayers(); |
||
| 153 | } |
||
| 154 | |||
| 155 | $this->logger->debug('done with this box'); |
||
| 156 | |||
| 157 | return PackedBox::fromPackedItemList($this->box, $this->getPackedItemList()); |
||
| 158 | } |
||
| 159 | |||
| 160 | /** |
||
| 161 | * Pack items into an individual vertical layer. |
||
| 162 | * |
||
| 163 | * @param int $startDepth |
||
| 164 | * @param int $widthLeft |
||
| 165 | * @param int $lengthLeft |
||
| 166 | * @param int $depthLeft |
||
| 167 | */ |
||
| 168 | protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft) |
||
| 169 | { |
||
| 170 | $this->layers[] = $layer = new PackedLayer(); |
||
| 171 | $prevItem = null; |
||
| 172 | $x = $y = $rowWidth = $rowLength = $layerDepth = 0; |
||
| 173 | |||
| 174 | while ($this->items->count() > 0) { |
||
| 175 | $itemToPack = $this->items->extract(); |
||
| 176 | |||
| 177 | //skip items that are simply too heavy or too large |
||
| 178 | if (!$this->checkNonDimensionalConstraints($itemToPack)) { |
||
| 179 | continue; |
||
| 180 | } |
||
| 181 | |||
| 182 | $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, !$this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $startDepth); |
||
| 183 | |||
| 184 | if ($orientatedItem instanceof OrientatedItem) { |
||
| 185 | $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth); |
||
| 186 | $layer->insert($packedItem); |
||
| 187 | $this->remainingWeight -= $orientatedItem->getItem()->getWeight(); |
||
| 188 | $widthLeft -= $orientatedItem->getWidth(); |
||
| 189 | |||
| 190 | $rowWidth += $orientatedItem->getWidth(); |
||
| 191 | $rowLength = max($rowLength, $orientatedItem->getLength()); |
||
| 192 | $layerDepth = max($layerDepth, $orientatedItem->getDepth()); |
||
| 193 | |||
| 194 | //allow items to be stacked in place within the same footprint up to current layer depth |
||
| 195 | $stackableDepth = $layerDepth - $orientatedItem->getDepth(); |
||
| 196 | $this->tryAndStackItemsIntoSpace($layer, $prevItem, $this->items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth(), $rowLength); |
||
| 197 | $x += $orientatedItem->getWidth(); |
||
| 198 | |||
| 199 | $prevItem = $packedItem; |
||
| 200 | if ($this->items->count() === 0) { |
||
| 201 | $this->rebuildItemList(); |
||
| 202 | } |
||
| 203 | } elseif (count($layer->getItems()) === 0) { // zero items on layer |
||
| 204 | $this->logger->debug("doesn't fit on layer even when empty, skipping for good"); |
||
| 205 | continue; |
||
| 206 | } elseif ($widthLeft > 0 && $this->items->count() > 0) { // skip for now, move on to the next item |
||
| 207 | $this->logger->debug("doesn't fit, skipping for now"); |
||
| 208 | $this->skippedItems[] = $itemToPack; |
||
| 209 | // abandon here if next item is the same, no point trying to keep going. Last time is not skipped, need that to trigger appropriate reset logic |
||
| 210 | while ($this->items->count() > 2 && $this->orientatedItemFactory->isSameDimensions($itemToPack, $this->items->top())) { |
||
| 211 | $this->skippedItems[] = $this->items->extract(); |
||
| 212 | } |
||
| 213 | } elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength(), $itemToPack->getDepth())) { |
||
| 214 | $this->logger->debug('No more fit in width wise, resetting for new row'); |
||
| 215 | $widthLeft += $rowWidth; |
||
| 216 | $lengthLeft -= $rowLength; |
||
| 217 | $y += $rowLength; |
||
| 218 | $x = $rowWidth = $rowLength = 0; |
||
| 219 | $this->skippedItems[] = $itemToPack; |
||
| 220 | $this->rebuildItemList(); |
||
| 221 | $prevItem = null; |
||
| 222 | continue; |
||
| 223 | } else { |
||
| 224 | $this->logger->debug('no items fit, so starting next vertical layer'); |
||
| 225 | $this->skippedItems[] = $itemToPack; |
||
| 226 | $this->rebuildItemList(); |
||
| 227 | |||
| 228 | return; |
||
| 229 | } |
||
| 230 | } |
||
| 231 | } |
||
| 232 | |||
| 233 | /** |
||
| 234 | * During packing, it is quite possible that layers have been created that aren't physically stable |
||
| 235 | * i.e. they overhang the ones below. |
||
| 236 | * |
||
| 237 | * This function reorders them so that the ones with the greatest surface area are placed at the bottom |
||
| 238 | */ |
||
| 239 | public function stabiliseLayers() |
||
| 240 | { |
||
| 241 | $stabiliser = new LayerStabiliser(); |
||
| 242 | $this->layers = $stabiliser->stabilise($this->layers); |
||
| 243 | } |
||
| 244 | |||
| 245 | /** |
||
| 246 | * @param Item $itemToPack |
||
| 247 | * @param PackedItem|null $prevItem |
||
| 248 | * @param ItemList $nextItems |
||
| 249 | * @param bool $isLastItem |
||
| 250 | * @param int $maxWidth |
||
| 251 | * @param int $maxLength |
||
| 252 | * @param int $maxDepth |
||
| 253 | * @param int $rowLength |
||
| 254 | * @param int $x |
||
| 255 | * @param int $y |
||
| 256 | * @param int $z |
||
| 257 | * |
||
| 258 | * @return OrientatedItem|null |
||
| 259 | */ |
||
| 260 | protected function getOrientationForItem( |
||
| 261 | Item $itemToPack, |
||
| 262 | PackedItem $prevItem = null, |
||
| 263 | ItemList $nextItems, |
||
| 264 | $isLastItem, |
||
| 265 | $maxWidth, |
||
| 266 | $maxLength, |
||
| 267 | $maxDepth, |
||
| 268 | $rowLength, |
||
| 269 | $x, |
||
| 270 | $y, |
||
| 271 | $z |
||
| 272 | ) { |
||
| 273 | $this->logger->debug( |
||
| 274 | "evaluating item {$itemToPack->getDescription()} for fit", |
||
| 275 | [ |
||
| 276 | 'item' => $itemToPack, |
||
| 277 | 'space' => [ |
||
| 278 | 'maxWidth' => $maxWidth, |
||
| 279 | 'maxLength' => $maxLength, |
||
| 280 | 'maxDepth' => $maxDepth, |
||
| 281 | ], |
||
| 282 | ] |
||
| 283 | ); |
||
| 284 | |||
| 285 | $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null; |
||
| 286 | $prevPackedItemList = $itemToPack instanceof ConstrainedPlacementItem ? $this->getPackedItemList() : new PackedItemList(); // don't calculate it if not going to be used |
||
| 287 | |||
| 288 | $orientatedItemDecision = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth, $rowLength, $x, $y, $z, $prevPackedItemList); |
||
| 289 | |||
| 290 | return $orientatedItemDecision; |
||
| 291 | } |
||
| 292 | |||
| 293 | /** |
||
| 294 | * Figure out if we can stack the next item vertically on top of this rather than side by side |
||
| 295 | * Used when we've packed a tall item, and have just put a shorter one next to it. |
||
| 296 | * |
||
| 297 | * @param PackedLayer $layer |
||
| 298 | * @param PackedItem|null $prevItem |
||
| 299 | * @param ItemList $nextItems |
||
| 300 | * @param int $maxWidth |
||
| 301 | * @param int $maxLength |
||
| 302 | * @param int $maxDepth |
||
| 303 | * @param int $x |
||
| 304 | * @param int $y |
||
| 305 | * @param int $z |
||
| 306 | */ |
||
| 307 | protected function tryAndStackItemsIntoSpace( |
||
| 308 | PackedLayer $layer, |
||
| 309 | PackedItem $prevItem = null, |
||
| 310 | ItemList $nextItems, |
||
| 311 | $maxWidth, |
||
| 312 | $maxLength, |
||
| 313 | $maxDepth, |
||
| 314 | $x, |
||
| 315 | $y, |
||
| 316 | $z, |
||
| 317 | $rowLength |
||
| 318 | ) { |
||
| 319 | while ($this->items->count() > 0 && $this->checkNonDimensionalConstraints($this->items->top())) { |
||
| 320 | $stackedItem = $this->getOrientationForItem( |
||
| 321 | $this->items->top(), |
||
| 322 | $prevItem, |
||
| 323 | $nextItems, |
||
| 324 | $this->items->count() === 1, |
||
| 325 | $maxWidth, |
||
| 326 | $maxLength, |
||
| 327 | $maxDepth, |
||
| 328 | $rowLength, |
||
| 329 | $x, |
||
| 330 | $y, |
||
| 331 | $z |
||
| 332 | ); |
||
| 333 | if ($stackedItem) { |
||
| 334 | $this->remainingWeight -= $this->items->top()->getWeight(); |
||
| 335 | $layer->insert(PackedItem::fromOrientatedItem($stackedItem, $x, $y, $z)); |
||
| 336 | $this->items->extract(); |
||
| 337 | $maxDepth -= $stackedItem->getDepth(); |
||
| 338 | $z += $stackedItem->getDepth(); |
||
| 339 | } else { |
||
| 340 | break; |
||
| 341 | } |
||
| 342 | } |
||
| 343 | } |
||
| 344 | |||
| 345 | /** |
||
| 346 | * As well as purely dimensional constraints, there are other constraints that need to be met |
||
| 347 | * e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box). |
||
| 348 | * |
||
| 349 | * @param Item $itemToPack |
||
| 350 | * |
||
| 351 | * @return bool |
||
| 352 | */ |
||
| 353 | protected function checkNonDimensionalConstraints(Item $itemToPack) |
||
| 354 | { |
||
| 355 | $weightOK = $itemToPack->getWeight() <= $this->remainingWeight; |
||
| 356 | |||
| 357 | if ($itemToPack instanceof ConstrainedItem) { |
||
| 358 | return $weightOK && $itemToPack->canBePackedInBox($this->getPackedItemList()->asItemList(), $this->box); |
||
| 359 | } |
||
| 360 | |||
| 361 | return $weightOK; |
||
| 362 | } |
||
| 363 | |||
| 364 | /** |
||
| 365 | * Check the item physically fits in the box (at all). |
||
| 366 | * |
||
| 367 | * @param Item $itemToPack |
||
| 368 | * |
||
| 369 | * @return bool |
||
| 370 | */ |
||
| 371 | protected function checkDimensionalConstraints(Item $itemToPack) |
||
| 372 | { |
||
| 373 | $orientatedItemFactory = new OrientatedItemFactory($this->box); |
||
| 374 | $orientatedItemFactory->setLogger($this->logger); |
||
| 375 | |||
| 376 | return (bool) $orientatedItemFactory->getPossibleOrientationsInEmptyBox($itemToPack); |
||
| 377 | } |
||
| 378 | |||
| 379 | /** |
||
| 380 | * Reintegrate skipped items into main list. |
||
| 381 | */ |
||
| 382 | protected function rebuildItemList() |
||
| 383 | { |
||
| 384 | while(count($this->skippedItems)) { |
||
| 385 | $this->items->insert(array_pop($this->skippedItems)); |
||
| 386 | } |
||
| 387 | } |
||
| 388 | |||
| 389 | /** |
||
| 390 | * Swap back width/length of the packed items to match orientation of the box if needed. |
||
| 391 | */ |
||
| 392 | protected function rotateLayersNinetyDegrees() |
||
| 393 | { |
||
| 394 | foreach ($this->layers as $i => $originalLayer) { |
||
| 395 | $newLayer = new PackedLayer(); |
||
| 396 | foreach ($originalLayer->getItems() as $item) { |
||
| 397 | $packedItem = new PackedItem($item->getItem(), $item->getY(), $item->getX(), $item->getZ(), $item->getLength(), $item->getWidth(), $item->getDepth()); |
||
| 398 | $newLayer->insert($packedItem); |
||
| 399 | } |
||
| 400 | $this->layers[$i] = $newLayer; |
||
| 401 | } |
||
| 402 | } |
||
| 403 | |||
| 404 | /** |
||
| 405 | * Are there items left to pack? |
||
| 406 | * |
||
| 407 | * @return bool |
||
| 408 | */ |
||
| 409 | protected function hasItemsLeftToPack() |
||
| 410 | { |
||
| 411 | return count($this->skippedItems) + $this->items->count() > 0; |
||
| 412 | } |
||
| 413 | |||
| 414 | /** |
||
| 415 | * Generate a single list of items packed. |
||
| 416 | * |
||
| 417 | * @return PackedItemList |
||
| 418 | */ |
||
| 419 | protected function getPackedItemList() |
||
| 429 | } |
||
| 430 | |||
| 431 | /** |
||
| 432 | * Return the current packed depth. |
||
| 433 | * |
||
| 434 | * @return int |
||
| 435 | */ |
||
| 436 | protected function getCurrentPackedDepth() |
||
| 444 | } |
||
| 445 | } |
||
| 446 |