/** * Pack as many items as possible into specific given box * @param Box $aBox * @param ItemList $aItems * @return PackedBox packed box */ public function packIntoBox(Box $aBox, ItemList $aItems) { $this->logger->log(LogLevel::DEBUG, "[EVALUATING BOX] {$aBox->getReference()}"); $packedItems = new ItemList(); $remainingDepth = $aBox->getInnerDepth(); $remainingWeight = $aBox->getMaxWeight() - $aBox->getEmptyWeight(); $remainingWidth = $aBox->getInnerWidth(); $remainingLength = $aBox->getInnerLength(); $layerWidth = $layerLength = $layerDepth = 0; while (!$aItems->isEmpty()) { $itemToPack = $aItems->top(); if ($itemToPack->getDepth() > $remainingDepth || $itemToPack->getWeight() > $remainingWeight) { break; } $this->logger->log(LogLevel::DEBUG, "evaluating item {$itemToPack->getDescription()}"); $this->logger->log(LogLevel::DEBUG, "remaining width: {$remainingWidth}, length: {$remainingLength}, depth: {$remainingDepth}"); $this->logger->log(LogLevel::DEBUG, "layerWidth: {$layerWidth}, layerLength: {$layerLength}, layerDepth: {$layerDepth}"); $itemWidth = $itemToPack->getWidth(); $itemLength = $itemToPack->getLength(); $fitsSameGap = min($remainingWidth - $itemWidth, $remainingLength - $itemLength); $fitsRotatedGap = min($remainingWidth - $itemLength, $remainingLength - $itemWidth); if ($fitsSameGap >= 0 || $fitsRotatedGap >= 0) { $packedItems->insert($aItems->extract()); $remainingWeight -= $itemToPack->getWeight(); if ($fitsRotatedGap < 0 || $fitsSameGap >= 0 && $fitsSameGap <= $fitsRotatedGap || !$aItems->isEmpty() && $aItems->top() == $itemToPack && $remainingLength >= 2 * $itemLength) { $this->logger->log(LogLevel::DEBUG, "fits (better) unrotated"); $remainingLength -= $itemLength; $layerLength += $itemLength; $layerWidth = max($itemWidth, $layerWidth); } else { $this->logger->log(LogLevel::DEBUG, "fits (better) rotated"); $remainingLength -= $itemWidth; $layerLength += $itemWidth; $layerWidth = max($itemLength, $layerWidth); } $layerDepth = max($layerDepth, $itemToPack->getDepth()); //greater than 0, items will always be less deep //allow items to be stacked in place within the same footprint up to current layerdepth $maxStackDepth = $layerDepth - $itemToPack->getDepth(); while (!$aItems->isEmpty()) { $potentialStackItem = $aItems->top(); if ($potentialStackItem->getDepth() <= $maxStackDepth && $potentialStackItem->getWeight() <= $remainingWeight && $potentialStackItem->getWidth() <= $itemToPack->getWidth() && $potentialStackItem->getLength() <= $itemToPack->getLength()) { $remainingWeight -= $potentialStackItem->getWeight(); $maxStackDepth -= $potentialStackItem->getDepth(); $packedItems->insert($aItems->extract()); } else { break; } } } else { if ($remainingWidth >= min($itemWidth, $itemLength) && $layerDepth > 0 && $layerWidth > 0 && $layerLength > 0) { $this->logger->log(LogLevel::DEBUG, "No more fit in lengthwise, resetting for new row"); $remainingLength += $layerLength; $remainingWidth -= $layerWidth; $layerWidth = $layerLength = 0; continue; } if ($remainingLength < min($itemWidth, $itemLength) || $layerDepth == 0) { $this->logger->log(LogLevel::DEBUG, "doesn't fit on layer even when empty"); break; } $remainingWidth = $layerWidth ? min(floor($layerWidth * 1.1), $aBox->getInnerWidth()) : $aBox->getInnerWidth(); $remainingLength = $layerLength ? min(floor($layerLength * 1.1), $aBox->getInnerLength()) : $aBox->getInnerLength(); $remainingDepth -= $layerDepth; $layerWidth = $layerLength = $layerDepth = 0; $this->logger->log(LogLevel::DEBUG, "doesn't fit, so starting next vertical layer"); } } $this->logger->log(LogLevel::DEBUG, "done with this box"); return new PackedBox($aBox, $packedItems, $remainingWidth, $remainingLength, $remainingDepth, $remainingWeight); }
/** * Figure out if we can stack the next item vertically on top of this rather than side by side * Used when we've packed a tall item, and have just put a shorter one next to it * @param ItemList $packedItems * @param int $maxWidth * @param int $maxLength * @param int $maxDepth */ protected function tryAndStackItemsIntoSpace(ItemList $packedItems, $maxWidth, $maxLength, $maxDepth) { while (!$this->items->isEmpty() && $this->remainingWeight >= $this->items->top()->getWeight()) { $stackedItem = $this->findBestOrientation($this->items->top(), null, null, $maxWidth, $maxLength, $maxDepth); if ($stackedItem) { $this->remainingWeight -= $this->items->top()->getWeight(); $maxDepth -= $stackedItem->getDepth(); $packedItems->insert($this->items->extract()); } else { break; } } }
/** * Pack as many items as possible into specific given box * @param Box $box * @param ItemList $items * @return PackedBox packed box */ public function packIntoBox(Box $box, ItemList $items) { $this->logger->log(LogLevel::DEBUG, "[EVALUATING BOX] {$box->getReference()}"); $packedItems = new ItemList(); $remainingDepth = $box->getInnerDepth(); $remainingWeight = $box->getMaxWeight() - $box->getEmptyWeight(); $remainingWidth = $box->getInnerWidth(); $remainingLength = $box->getInnerLength(); $layerWidth = $layerLength = $layerDepth = 0; while (!$items->isEmpty()) { $itemToPack = $items->top(); //skip items that are simply too large if ($this->isItemTooLargeForBox($itemToPack, $remainingDepth, $remainingWeight)) { $items->extract(); continue; } $this->logger->log(LogLevel::DEBUG, "evaluating item {$itemToPack->getDescription()}"); $this->logger->log(LogLevel::DEBUG, "remaining width: {$remainingWidth}, length: {$remainingLength}, depth: {$remainingDepth}"); $this->logger->log(LogLevel::DEBUG, "layerWidth: {$layerWidth}, layerLength: {$layerLength}, layerDepth: {$layerDepth}"); $itemWidth = $itemToPack->getWidth(); $itemLength = $itemToPack->getLength(); if ($this->fitsGap($itemToPack, $remainingWidth, $remainingLength)) { $packedItems->insert($items->extract()); $remainingWeight -= $itemToPack->getWeight(); $nextItem = !$items->isEmpty() ? $items->top() : null; if ($this->fitsBetterRotated($itemToPack, $nextItem, $remainingWidth, $remainingLength)) { $this->logger->log(LogLevel::DEBUG, "fits (better) unrotated"); $remainingLength -= $itemLength; $layerLength += $itemLength; $layerWidth = max($itemWidth, $layerWidth); } else { $this->logger->log(LogLevel::DEBUG, "fits (better) rotated"); $remainingLength -= $itemWidth; $layerLength += $itemWidth; $layerWidth = max($itemLength, $layerWidth); } $layerDepth = max($layerDepth, $itemToPack->getDepth()); //greater than 0, items will always be less deep //allow items to be stacked in place within the same footprint up to current layerdepth $maxStackDepth = $layerDepth - $itemToPack->getDepth(); while (!$items->isEmpty() && $this->canStackItemInLayer($itemToPack, $items->top(), $maxStackDepth, $remainingWeight)) { $remainingWeight -= $items->top()->getWeight(); $maxStackDepth -= $items->top()->getDepth(); $packedItems->insert($items->extract()); } } else { if ($remainingWidth >= min($itemWidth, $itemLength) && $this->isLayerStarted($layerWidth, $layerLength, $layerDepth)) { $this->logger->log(LogLevel::DEBUG, "No more fit in lengthwise, resetting for new row"); $remainingLength += $layerLength; $remainingWidth -= $layerWidth; $layerWidth = $layerLength = 0; continue; } elseif ($remainingLength < min($itemWidth, $itemLength) || $layerDepth == 0) { $this->logger->log(LogLevel::DEBUG, "doesn't fit on layer even when empty"); $items->extract(); continue; } $remainingWidth = $layerWidth ? min(floor($layerWidth * 1.1), $box->getInnerWidth()) : $box->getInnerWidth(); $remainingLength = $layerLength ? min(floor($layerLength * 1.1), $box->getInnerLength()) : $box->getInnerLength(); $remainingDepth -= $layerDepth; $layerWidth = $layerLength = $layerDepth = 0; $this->logger->log(LogLevel::DEBUG, "doesn't fit, so starting next vertical layer"); } } $this->logger->log(LogLevel::DEBUG, "done with this box"); return new PackedBox($box, $packedItems, $remainingWidth, $remainingLength, $remainingDepth, $remainingWeight); }
/** * Pack items into boxes using the principle of largest volume item first * * @throws \RuntimeException * @return PackedBoxList */ public function doVolumePacking() { $packedBoxes = new PackedBoxList(); //Keep going until everything packed while ($this->items->count()) { $boxesToEvaluate = clone $this->boxes; $packedBoxesIteration = new PackedBoxList(); //Loop through boxes starting with smallest, see what happens while (!$boxesToEvaluate->isEmpty()) { $box = $boxesToEvaluate->extract(); $volumePacker = new VolumePacker($box, clone $this->items); $volumePacker->setLogger($this->logger); $packedBox = $volumePacker->pack(); if ($packedBox->getItems()->count()) { $packedBoxesIteration->insert($packedBox); //Have we found a single box that contains everything? if ($packedBox->getItems()->count() === $this->items->count()) { break; } } } //Check iteration was productive if ($packedBoxesIteration->isEmpty()) { throw new \RuntimeException('Item ' . $this->items->top()->getDescription() . ' is too large to fit into any box'); } //Find best box of iteration, and remove packed items from unpacked list $bestBox = $packedBoxesIteration->top(); $unPackedItems = $this->items->asArray(); foreach (clone $bestBox->getItems() as $packedItem) { foreach ($unPackedItems as $unpackedKey => $unpackedItem) { if ($packedItem === $unpackedItem) { unset($unPackedItems[$unpackedKey]); break; } } } $unpackedItemList = new ItemList(); foreach ($unPackedItems as $unpackedItem) { $unpackedItemList->insert($unpackedItem); } $this->items = $unpackedItemList; $packedBoxes->insert($bestBox); } return $packedBoxes; }