diff --git a/.travis.yml b/.travis.yml index 0734442c..0229340e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,41 @@ group: travis_latest language: php +env: + - TEST_SUITE=unit + - TEST_SUITE=functional + - TEST_SUITE=efficiency +php: + - 7.3 + - 7.2 + - 7.1 + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - nightly + matrix: include: - php: 5.4 env: lowest=1 - - php: 5.5 - - php: 5.6 - - php: 7.0 - - php: 7.1 - - php: 7.2 - - php: 7.3 + allow_failures: + - php: nightly cache: directories: - - $HOME/.cache/composer/files + - $HOME/.composer/cache + - $HOME/.cache/composer install: - phpenv config-rm xdebug.ini || true; - + - composer validate --strict; + - | + sed -i 's/"bin-compat" : "full"/"bin-compat" : "auto"/g' composer.json; + - | + if [ "$csfixer" != "1" ]; then + composer remove --dev friendsofphp/php-cs-fixer; + fi; - | if [ "$lowest" = "1" ]; then composer update --prefer-lowest --prefer-stable; @@ -26,12 +43,19 @@ install: composer update; fi; -before_script: +script: - | - if [ "$TRAVIS_PHP_VERSION" != "5.4" ] && [ "$TRAVIS_PHP_VERSION" != "5.5" ] && [ "$TRAVIS_PHP_VERSION" != "5.6" ]; then - echo "memory_limit = 3072M" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; + if [ "$csfixer" = "1" ]; then + vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --diff-format=udiff --allow-risky=yes; + fi; + - | + if [ "${TEST_SUITE}" = "unit" ]; then + php vendor/bin/phpunit --exclude-group efficiency; + elif [ "${TEST_SUITE}" = "efficiency" ]; then + php vendor/bin/phpunit --group efficiency; + elif [ "${TEST_SUITE}" = "functional" ]; then + php vendor/bin/behat --strict; + else + php vendor/bin/behat --strict; + php vendor/bin/phpunit; fi; - -script: - - php vendor/bin/phpunit; - - php vendor/bin/behat --strict; diff --git a/composer.json b/composer.json index 10258129..99ffd288 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "license": "MIT", "require": { "php": ">=5.4.0", - "psr/log": "^1.0" + "psr/log": "^1.0", + "ext-json": "*" }, "require-dev": { "behat/behat": "^3.4", @@ -20,6 +21,14 @@ "monolog/monolog": "^1.0", "phpunit/phpunit": "^4.8.35||^5.0||^6.0||^7.0" }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "bin-compat" : "full", + "optimize-autoloader": true + }, "autoload": { "psr-4": { "DVDoug\\BoxPacker\\": "src/", diff --git a/docs/advanced-usage.rst b/docs/advanced-usage.rst index eef0e5b5..7037dc08 100644 --- a/docs/advanced-usage.rst +++ b/docs/advanced-usage.rst @@ -10,8 +10,8 @@ alternative/additional sizes of box. At a high level, the ``getVolumeUtilisation()`` method exists which calculates how full the box is as a percentage of volume. -Lower-level methods are also available for examining this data in detail either using ``getUsed[Width|Length|Depth()`` -(a hypothetical box placed around the items) or ``getRemaining[Width|Length|Depth()`` (the difference between the dimensions of +Lower-level methods are also available for examining this data in detail either using ``getUsed[Width|Length|Depth()]`` +(a hypothetical box placed around the items) or ``getRemaining[Width|Length|Depth()]`` (the difference between the dimensions of the actual box and the hypothetical box). .. note:: @@ -37,7 +37,7 @@ Custom Constraints ------------------ For more advanced use cases where greater control over the contents of each box is required (e.g. legal limits on the number of -hazardous items per box, or perhaps fragile items requiring an extra-strong outer box) you may implement the ``BoxPacker\ConstrainedItem`` +hazardous items per box, or perhaps fragile items requiring an extra-strong outer box) you may implement the ``BoxPacker\ConstrainedPlacementItem`` interface which contains an additional callback method allowing you to decide whether to allow an item may be packed into a box or not. @@ -54,17 +54,32 @@ Example - only allow 2 batteries per box use DVDoug\BoxPacker\Item; use DVDoug\BoxPacker\ItemList; - class LithiumBattery implements ConstrainedItem + class LithiumBattery implements ConstrainedPlacementItem { /** - * @param ItemList $alreadyPackedItems - * @param TestBox $box + * Max 2 batteries per box. * + * @param Box $box + * @param PackedItemList $alreadyPackedItems + * @param int $proposedX + * @param int $proposedY + * @param int $proposedZ + * @param int $width + * @param int $length + * @param int $depth * @return bool */ - public function canBePackedInBox(ItemList $alreadyPackedItems, Box $box) - { + public function canBePacked( + Box $box, + PackedItemList $alreadyPackedItems, + int $proposedX, + int $proposedY, + int $proposedZ, + int $width, + int $length, + int $depth + ) { $batteriesPacked = 0; foreach ($alreadyPackedItems as $packedItem) { if ($packedItem instanceof LithiumBattery) { @@ -80,3 +95,59 @@ Example - only allow 2 batteries per box } } +Example - don't allow batteries to be stacked +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + getDescription() === 'Battery'; + } + ); + + /** @var PackedItem $alreadyPacked */ + foreach ($alreadyPackedType as $alreadyPacked) { + if ( + $alreadyPacked->getZ() + $alreadyPacked->getDepth() === $proposedZ && + $proposedX >= $alreadyPacked->getX() && $proposedX <= ($alreadyPacked->getX() + $alreadyPacked->getWidth()) && + $proposedY >= $alreadyPacked->getY() && $proposedY <= ($alreadyPacked->getY() + $alreadyPacked->getLength())) { + return false; + } + } + + return true; + } + } diff --git a/docs/conf.py b/docs/conf.py index 96d597dc..72fd03be 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,7 @@ # General information about the project. project = u'BoxPacker' -copyright = u'2018, Doug Wright' +copyright = u'2019, Doug Wright' author = u'Doug Wright' # The version info for the project you're documenting, acts as replacement for diff --git a/docs/index.rst b/docs/index.rst index 0dcf0121..15930072 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,7 +27,6 @@ BoxPacker is licensed under the `MIT license`_. installation principles getting-started - advanced-usage weight-distribution advanced-usage whatsnew diff --git a/license.txt b/license.txt index 5d2e77fb..462bacb5 100644 --- a/license.txt +++ b/license.txt @@ -1,4 +1,4 @@ -Copyright (C) 2012-2018 Doug Wright +Copyright (C) 2012-2019 Doug Wright Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/phpunit.xml b/phpunit.xml index d7796cb3..d2d70ded 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -14,5 +14,6 @@ + diff --git a/src/BoxList.php b/src/BoxList.php index f6f0f3d0..0a2b707f 100644 --- a/src/BoxList.php +++ b/src/BoxList.php @@ -26,12 +26,33 @@ class BoxList extends \SplMinHeap */ public function compare($boxA, $boxB) { - if ($boxB->getInnerVolume() > $boxA->getInnerVolume()) { + $boxAVolume = $boxA->getInnerWidth() * $boxA->getInnerLength() * $boxA->getInnerDepth(); + $boxBVolume = $boxB->getInnerWidth() * $boxB->getInnerLength() * $boxB->getInnerDepth(); + + // try smallest box first + if ($boxBVolume > $boxAVolume) { return 1; - } elseif ($boxB->getInnerVolume() < $boxA->getInnerVolume()) { + } + if ($boxAVolume > $boxBVolume) { + return -1; + } + + // smallest empty weight + if ($boxB->getEmptyWeight() > $boxA->getEmptyWeight()) { + return 1; + } + if ($boxA->getEmptyWeight() > $boxB->getEmptyWeight()) { return -1; - } else { - return 0; } + + // maximum weight capacity as fallback decider + if ($boxB->getMaxWeight() > $boxA->getMaxWeight()) { + return 1; + } + if ($boxA->getMaxWeight() > $boxB->getMaxWeight()) { + return -1; + } + + return 0; } } diff --git a/src/ConstrainedItem.php b/src/ConstrainedItem.php index b86de27b..613d61dc 100644 --- a/src/ConstrainedItem.php +++ b/src/ConstrainedItem.php @@ -4,13 +4,13 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; /** * An item to be packed where additional constraints need to be considered. Only implement this interface if you actually * need this additional functionality as it will slow down the packing algorithm. * + * @deprecated use ConstrainedPlacementItem instead which has additional flexibility * @author Doug Wright */ interface ConstrainedItem extends Item diff --git a/src/ConstrainedPlacementItem.php b/src/ConstrainedPlacementItem.php new file mode 100644 index 00000000..3cf21f8b --- /dev/null +++ b/src/ConstrainedPlacementItem.php @@ -0,0 +1,40 @@ + batteries per box. + * + * @param Box $box + * @param PackedItemList $alreadyPackedItems + * @param int $proposedX + * @param int $proposedY + * @param int $proposedZ + * @param int $width + * @param int $length + * @param int $depth + * @return bool + */ + public function canBePacked( + Box $box, + PackedItemList $alreadyPackedItems, + $proposedX, + $proposedY, + $proposedZ, + $width, + $length, + $depth + ); +} diff --git a/src/ItemList.php b/src/ItemList.php index 77bf06a0..7ec9c3ba 100644 --- a/src/ItemList.php +++ b/src/ItemList.php @@ -73,4 +73,26 @@ public function topN($n) return $topNList; } + /** + * Remove item from list. + * + * @param Item $item + */ + public function remove(Item $item) + { + $workingSet = []; + while (!$this->isEmpty()) { + $workingSet[] = $this->extract(); + } + + $removed = false; // there can be multiple identical items, ensure that only 1 is removed + foreach ($workingSet as $workingSetItem) { + if (!$removed && $workingSetItem === $item) { + $removed = true; + } else { + $this->insert($workingSetItem); + } + } + + } } diff --git a/src/OrientatedItem.php b/src/OrientatedItem.php index 7a175760..95402e84 100644 --- a/src/OrientatedItem.php +++ b/src/OrientatedItem.php @@ -7,12 +7,14 @@ namespace DVDoug\BoxPacker; +use JsonSerializable; + /** * An item to be packed. * * @author Doug Wright */ -class OrientatedItem +class OrientatedItem implements JsonSerializable { /** * @var Item @@ -34,7 +36,6 @@ class OrientatedItem */ protected $depth; - /** * @var float[] */ @@ -111,7 +112,7 @@ public function getSurfaceFootprint() */ public function getTippingPoint() { - $cacheKey = $this->width . '|' . $this->length . '|' . $this->depth; + $cacheKey = $this->width . '|' . $this->length . '|' . $this->depth; if (isset(static::$tippingPointCache[$cacheKey])) { $tippingPoint = static::$tippingPointCache[$cacheKey]; @@ -134,4 +135,25 @@ public function isStable() { return $this->getTippingPoint() > 0.261; } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return [ + 'item' => $this->item, + 'width' => $this->width, + 'length' => $this->length, + 'depth' => $this->depth, + ]; + } + + /** + * @return string + */ + public function __toString() + { + return $this->width . '|' . $this->length . '|' . $this->depth; + } } diff --git a/src/OrientatedItemFactory.php b/src/OrientatedItemFactory.php index 44ef3115..34cb0d5e 100644 --- a/src/OrientatedItemFactory.php +++ b/src/OrientatedItemFactory.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use Psr\Log\LoggerAwareInterface; @@ -43,6 +42,11 @@ public function __construct(Box $box) * @param int $widthLeft * @param int $lengthLeft * @param int $depthLeft + * @param int $rowLength + * @param int $x + * @param int $y + * @param int $z + * @param PackedItemList $prevPackedItemList * * @return OrientatedItem|null */ @@ -53,16 +57,21 @@ public function getBestOrientation( $isLastItem, $widthLeft, $lengthLeft, - $depthLeft + $depthLeft, + $rowLength, + $x, + $y, + $z, + PackedItemList $prevPackedItemList ) { - $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft); + $possibleOrientations = $this->getPossibleOrientations($item, $prevItem, $widthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList); $usableOrientations = $this->getUsableOrientations($item, $possibleOrientations, $isLastItem); if (empty($usableOrientations)) { return null; } - usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItems) { + usort($usableOrientations, function (OrientatedItem $a, OrientatedItem $b) use ($widthLeft, $lengthLeft, $depthLeft, $nextItems, $rowLength, $x, $y, $z, $prevPackedItemList) { $orientationAWidthLeft = $widthLeft - $a->getWidth(); $orientationALengthLeft = $lengthLeft - $a->getLength(); $orientationADepthLeft = $depthLeft - $a->getDepth(); @@ -82,8 +91,8 @@ public function getBestOrientation( // prefer leaving room for next item in current row if ($nextItems->count()) { - $nextItemFitA = count($this->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft)); - $nextItemFitB = count($this->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft)); + $nextItemFitA = count($this->getPossibleOrientations($nextItems->top(), $a, $orientationAWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList)); + $nextItemFitB = count($this->getPossibleOrientations($nextItems->top(), $b, $orientationBWidthLeft, $lengthLeft, $depthLeft, $x, $y, $z, $prevPackedItemList)); if ($nextItemFitA && !$nextItemFitB) { return -1; } @@ -92,10 +101,16 @@ public function getBestOrientation( } // if not an easy either/or, do a partial lookahead - $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft); - $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft); - if ($additionalPackedA !== $additionalPackedB) { - return $additionalPackedB - $additionalPackedA; + $additionalPackedA = $this->calculateAdditionalItemsPackedWithThisOrientation($a, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength); + $additionalPackedB = $this->calculateAdditionalItemsPackedWithThisOrientation($b, $nextItems, $widthLeft, $lengthLeft, $depthLeft, $rowLength); + if ($additionalPackedA > $additionalPackedB) { + return -1; + } + if ($additionalPackedA < $additionalPackedB) { + return 1; + } + if ($additionalPackedA === 0) { + return PHP_MAJOR_VERSION > 5 ? -1 : 1; } } // otherwise prefer leaving minimum possible gap, or the greatest footprint @@ -116,6 +131,10 @@ public function getBestOrientation( * @param int $widthLeft * @param int $lengthLeft * @param int $depthLeft + * @param int $x + * @param int $y + * @param int $z + * @param PackedItemList $prevPackedItemList * * @return OrientatedItem[] */ @@ -124,7 +143,11 @@ public function getPossibleOrientations( OrientatedItem $prevItem = null, $widthLeft, $lengthLeft, - $depthLeft + $depthLeft, + $x, + $y, + $z, + PackedItemList $prevPackedItemList ) { $orientations = []; @@ -137,10 +160,24 @@ public function getPossibleOrientations( $orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth()); } + $orientations = array_unique($orientations); + //remove any that simply don't fit - return array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) { + $orientations = array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) { return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft; }); + + if ($item instanceof ConstrainedPlacementItem) { + $box = $this->box; + $orientations = array_filter($orientations, function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) { + /** @var ConstrainedPlacementItem $constrainedItem */ + $constrainedItem = $i->getItem(); + + return $constrainedItem->canBePacked($box, $prevPackedItemList, $x, $y, $z, $i->getWidth(), $i->getLength(), $i->getDepth()); + }); + } + + return $orientations; } /** @@ -169,7 +206,11 @@ public function getPossibleOrientationsInEmptyBox(Item $item) null, $this->box->getInnerWidth(), $this->box->getInnerLength(), - $this->box->getInnerDepth() + $this->box->getInnerDepth(), + 0, + 0, + 0, + new PackedItemList() ); static::$emptyBoxCache[$cacheKey] = $orientations; } @@ -265,6 +306,7 @@ protected function isSameDimensions(Item $itemA, Item $itemB) * @param int $originalWidthLeft * @param int $originalLengthLeft * @param int $depthLeft + * @param int $currentRowLengthBeforePacking * @return int */ protected function calculateAdditionalItemsPackedWithThisOrientation( @@ -272,40 +314,35 @@ protected function calculateAdditionalItemsPackedWithThisOrientation( ItemList $nextItems, $originalWidthLeft, $originalLengthLeft, - $depthLeft + $depthLeft, + $currentRowLengthBeforePacking ) { $packedCount = 0; - // first try packing into current row - $currentRowWorkingSetItems = $nextItems->topN(8); // cap lookahead as this gets recursive and slow - $nextRowWorkingSetItems = new ItemList(); - $widthLeft = $originalWidthLeft - $prevItem->getWidth(); - $lengthLeft = $originalLengthLeft; - while (count($currentRowWorkingSetItems) > 0 && $widthLeft > 0) { - $itemToPack = $currentRowWorkingSetItems->extract(); - $orientatedItem = $this->getBestOrientation($itemToPack, $prevItem, $currentRowWorkingSetItems, !count($currentRowWorkingSetItems), $widthLeft, $lengthLeft, $depthLeft); - if ($orientatedItem instanceof OrientatedItem) { - ++$packedCount; - $widthLeft -= $orientatedItem->getWidth(); - $prevItem = $orientatedItem; - } else { - $nextRowWorkingSetItems->insert($itemToPack); - } + $currentRowLength = max($prevItem->getLength(), $currentRowLengthBeforePacking); + + $itemsToPack = $nextItems->topN(8); // cap lookahead as this gets recursive and slow + + $tempBox = new WorkingVolume($originalWidthLeft - $prevItem->getWidth(), $currentRowLength, $depthLeft, PHP_INT_MAX); + $tempPacker = new VolumePacker($tempBox, clone $itemsToPack); + $tempPacker->setLookAheadMode(true); + $remainingRowPacked = $tempPacker->pack(); + /** @var PackedItem $packedItem */ + foreach ($remainingRowPacked->getItems() as $packedItem) { + $itemsToPack->remove($packedItem); } - // then see what happens if we try in the next row - $widthLeft = $originalWidthLeft; - $lengthLeft = $originalLengthLeft - $prevItem->getLength(); - while (count($nextRowWorkingSetItems) > 0 && $widthLeft > 0) { - $itemToPack = $nextRowWorkingSetItems->extract(); - $orientatedItem = $this->getBestOrientation($itemToPack, $prevItem, $nextRowWorkingSetItems, !count($nextRowWorkingSetItems), $widthLeft, $lengthLeft, $depthLeft); - if ($orientatedItem instanceof OrientatedItem) { - ++$packedCount; - $widthLeft -= $orientatedItem->getWidth(); - $prevItem = $orientatedItem; - } + $tempBox = new WorkingVolume($originalWidthLeft, $originalLengthLeft - $currentRowLength, $depthLeft, PHP_INT_MAX); + $tempPacker = new VolumePacker($tempBox, clone $itemsToPack); + $tempPacker->setLookAheadMode(true); + $nextRowsPacked = $tempPacker->pack(); + /** @var PackedItem $packedItem */ + foreach ($nextRowsPacked->getItems() as $packedItem) { + $itemsToPack->remove($packedItem); } - return $packedCount; // this isn't scientific, but is a reasonable proxy for success from an actual forward packing + $this->logger->debug('Lookahead with orientation', ['packedCount' => $packedCount, 'orientatedItem' => $prevItem]); + + return $nextItems->count() - $itemsToPack->count(); } } diff --git a/src/PackedBox.php b/src/PackedBox.php index 13ded40e..a56ab9e2 100644 --- a/src/PackedBox.php +++ b/src/PackedBox.php @@ -7,6 +7,8 @@ namespace DVDoug\BoxPacker; +use RuntimeException; + /** * A "box" with items. * @@ -91,6 +93,11 @@ class PackedBox */ protected $usedDepth; + /** + * @var PackedItemList + */ + protected $packedItemList; + /** * Get box used. * @@ -261,6 +268,25 @@ public function getVolumeUtilisation() return round($itemVolume / $this->box->getInnerVolume() * 100, 1); } + /** + * @return PackedItemList + */ + public function getPackedItems() + { + if (!$this->packedItemList instanceof PackedItemList) { + throw new RuntimeException('No PackedItemList was set. Are you using the old constructor?'); + } + return $this->packedItemList; + } + + /** + * @param PackedItemList $packedItemList + */ + public function setPackedItems(PackedItemList $packedItemList) + { + $this->packedItemList = $packedItemList; + } + /** * Legacy constructor. * @@ -328,6 +354,7 @@ public static function fromPackedItemList(Box $box, PackedItemList $packedItems) $maxLength, $maxDepth ); + $packedBox->setPackedItems($packedItems); return $packedBox; } diff --git a/src/PackedLayer.php b/src/PackedLayer.php index 134269c2..02e69b07 100644 --- a/src/PackedLayer.php +++ b/src/PackedLayer.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; /** diff --git a/src/Packer.php b/src/Packer.php index 9dfb235c..31bf1c21 100644 --- a/src/Packer.php +++ b/src/Packer.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use Psr\Log\LoggerAwareInterface; @@ -64,7 +63,7 @@ public function addItem(Item $item, $qty = 1) for ($i = 0; $i < $qty; ++$i) { $this->items->insert($item); } - $this->logger->log(LogLevel::INFO, "added {$qty} x {$item->getDescription()}"); + $this->logger->log(LogLevel::INFO, "added {$qty} x {$item->getDescription()}", ['item' => $item]); } /** @@ -92,7 +91,7 @@ public function setItems($items) public function addBox(Box $box) { $this->boxes->insert($box); - $this->logger->log(LogLevel::INFO, "added box {$box->getReference()}"); + $this->logger->log(LogLevel::INFO, "added box {$box->getReference()}", ['box' => $box]); } /** @@ -141,7 +140,7 @@ public function pack() $packedBoxes = $redistributor->redistributeWeight($packedBoxes); } - $this->logger->log(LogLevel::INFO, "packing completed, {$packedBoxes->count()} boxes"); + $this->logger->log(LogLevel::INFO, "[PACKING COMPLETED], {$packedBoxes->count()} boxes"); return $packedBoxes; } diff --git a/src/VolumePacker.php b/src/VolumePacker.php index b97fea76..0d2ec125 100644 --- a/src/VolumePacker.php +++ b/src/VolumePacker.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use Psr\Log\LoggerAwareInterface; @@ -70,6 +69,13 @@ class VolumePacker implements LoggerAwareInterface */ protected $layers = []; + /** + * Whether the packer is in look-ahead mode (i.e. working ahead of the main packing). + * + * @var bool + */ + protected $lookAheadMode = false; + /** * Constructor. * @@ -93,6 +99,15 @@ public function __construct(Box $box, ItemList $items) } } + /** + * @internal + * @param bool $lookAhead + */ + public function setLookAheadMode($lookAhead) + { + $this->lookAheadMode = $lookAhead; + } + /** * Pack as many items as possible into specific given box. * @@ -100,7 +115,7 @@ public function __construct(Box $box, ItemList $items) */ public function pack() { - $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}"); + $this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]); while (count($this->items) > 0) { $layerStartDepth = $this->getCurrentPackedDepth(); @@ -111,7 +126,9 @@ public function pack() $this->rotateLayersNinetyDegrees(); } - $this->stabiliseLayers(); + if (!$this->lookAheadMode) { + $this->stabiliseLayers(); + } $this->logger->debug('done with this box'); @@ -141,7 +158,7 @@ protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft) continue; } - $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, $this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft); + $orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, $this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $startDepth); if ($orientatedItem instanceof OrientatedItem) { $packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth); @@ -155,7 +172,7 @@ protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft) //allow items to be stacked in place within the same footprint up to current layer depth $stackableDepth = $layerDepth - $orientatedItem->getDepth(); - $this->tryAndStackItemsIntoSpace($layer, $prevItem, $this->items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth()); + $this->tryAndStackItemsIntoSpace($layer, $prevItem, $this->items, $orientatedItem->getWidth(), $orientatedItem->getLength(), $stackableDepth, $x, $y, $startDepth + $orientatedItem->getDepth(), $rowLength); $x += $orientatedItem->getWidth(); $prevItem = $packedItem; @@ -204,6 +221,10 @@ public function stabiliseLayers() * @param int $maxWidth * @param int $maxLength * @param int $maxDepth + * @param int $rowLength + * @param int $x + * @param int $y + * @param int $z * * @return OrientatedItem|null */ @@ -214,7 +235,11 @@ protected function getOrientationForItem( $isLastItem, $maxWidth, $maxLength, - $maxDepth + $maxDepth, + $rowLength, + $x, + $y, + $z ) { $this->logger->debug( "evaluating item {$itemToPack->getDescription()} for fit", @@ -229,10 +254,11 @@ protected function getOrientationForItem( ); $prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null; + $prevPackedItemList = $itemToPack instanceof ConstrainedPlacementItem ? $this->getPackedItemList() : new PackedItemList(); // don't calculate it if not going to be used $orientatedItemFactory = new OrientatedItemFactory($this->box); $orientatedItemFactory->setLogger($this->logger); - $orientatedItemDecision = $orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth); + $orientatedItemDecision = $orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth, $rowLength, $x, $y, $z, $prevPackedItemList); return $orientatedItemDecision; } @@ -260,7 +286,8 @@ protected function tryAndStackItemsIntoSpace( $maxDepth, $x, $y, - $z + $z, + $rowLength ) { while (count($this->items) > 0 && $this->checkNonDimensionalConstraints($this->items->top())) { $stackedItem = $this->getOrientationForItem( @@ -270,7 +297,11 @@ protected function tryAndStackItemsIntoSpace( $this->items->count() === 1, $maxWidth, $maxLength, - $maxDepth + $maxDepth, + $rowLength, + $x, + $y, + $z ); if ($stackedItem) { $this->remainingWeight -= $this->items->top()->getWeight(); diff --git a/src/WeightRedistributor.php b/src/WeightRedistributor.php index 1cc5c085..945c61e6 100644 --- a/src/WeightRedistributor.php +++ b/src/WeightRedistributor.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use Psr\Log\LoggerAwareInterface; diff --git a/src/WorkingVolume.php b/src/WorkingVolume.php new file mode 100644 index 00000000..68bc8ccf --- /dev/null +++ b/src/WorkingVolume.php @@ -0,0 +1,150 @@ +width = $width; + $this->length = $length; + $this->depth = $depth; + $this->maxWeight = $maxWeight; + } + + /** + * @return string + */ + public function getReference() + { + return 'Working Volume'; + } + + /** + * @return int + */ + public function getOuterWidth() + { + return $this->width; + } + + /** + * @return int + */ + public function getOuterLength() + { + return $this->length; + } + + /** + * @return int + */ + public function getOuterDepth() + { + return $this->depth; + } + + /** + * @return int + */ + public function getEmptyWeight() + { + return 0; + } + + /** + * @return int + */ + public function getInnerWidth() + { + return $this->width; + } + + /** + * @return int + */ + public function getInnerLength() + { + return $this->length; + } + + /** + * @return int + */ + public function getInnerDepth() + { + return $this->depth; + } + + /** + * @return int + */ + public function getMaxWeight() + { + return $this->maxWeight; + } + + /** + * @return int + */ + public function getInnerVolume() + { + return $this->width * $this->length * $this->depth; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return [ + 'reference' => $this->getReference(), + 'width' => $this->width, + 'length' => $this->length, + 'depth' => $this->depth, + 'maxWeight' => $this->maxWeight, + ]; + } +} diff --git a/tests/BoxListTest.php b/tests/BoxListTest.php index b848979a..9125151e 100644 --- a/tests/BoxListTest.php +++ b/tests/BoxListTest.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use DVDoug\BoxPacker\Test\TestBox; @@ -75,4 +74,22 @@ public function testIssue30B() $sorted = iterator_to_array($list, false); self::assertEquals([$box1, $box3, $box2], $sorted); } + + /** + * Test that sorting of boxes with identical dimensions works as expected i.e. order by maximum weight capacity. + */ + public function testIssue163() + { + $box2 = new TestBox('Box2', 202, 152, 32, 10, 200, 150, 30, 100); + $box3 = new TestBox('Box3', 202, 152, 32, 10, 200, 150, 30, 250); + $box1 = new TestBox('Box1', 202, 152, 32, 10, 200, 150, 30, 50); + + $list = new BoxList(); + $list->insert($box1); + $list->insert($box2); + $list->insert($box3); + + $sorted = iterator_to_array($list, false); + self::assertEquals([$box1, $box2, $box3], $sorted); + } } diff --git a/tests/EfficiencyTest.php b/tests/EfficiencyTest.php index 9df93b01..f42c8a75 100644 --- a/tests/EfficiencyTest.php +++ b/tests/EfficiencyTest.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use DVDoug\BoxPacker\Test\TestBox; @@ -59,7 +58,7 @@ public function testCanPackRepresentativeLargerSamples( $packedItemCount2D += $packedBox->getItems()->count(); } - self::assertEquals($expectedBoxes2D, $packedBoxes2D->count()); + self::assertCount($expectedBoxes2D, $packedBoxes2D); self::assertEquals($expectedItemCount, $packedItemCount2D); self::assertEquals($expectedVolumeUtilisation2D, $packedBoxes2D->getVolumeUtilisation()); self::assertEquals($expectedWeightVariance2D, $packedBoxes2D->getWeightVariance()); @@ -69,7 +68,7 @@ public function getSamples() { $expected = ['2D' => [], '3D' => []]; - $expected2DData = fopen(__DIR__.'/data/expected.csv', 'r'); + $expected2DData = fopen(__DIR__ . '/data/expected.csv', 'rb'); while ($data = fgetcsv($expected2DData)) { $expected['2D'][$data[0]] = ['boxes' => $data[1], 'weightVariance' => $data[2], 'utilisation' => $data[3]]; $expected['3D'][$data[0]] = ['boxes' => $data[4], 'weightVariance' => $data[5], 'utilisation' => $data[6]]; @@ -77,7 +76,7 @@ public function getSamples() fclose($expected2DData); $boxes = []; - $boxData = fopen(__DIR__.'/data/boxes.csv', 'r'); + $boxData = fopen(__DIR__ . '/data/boxes.csv', 'rb'); while ($data = fgetcsv($boxData)) { $boxes[] = new TestBox( $data[0], @@ -94,35 +93,35 @@ public function getSamples() fclose($boxData); $tests = []; - $itemData = fopen(__DIR__.'/data/items.csv', 'r'); + $itemData = fopen(__DIR__ . '/data/items.csv', 'rb'); while ($data = fgetcsv($itemData)) { if (isset($tests[$data[0]])) { $tests[$data[0]]['items'][] = [ - 'qty' => $data[1], - 'name' => $data[2], - 'width' => $data[3], + 'qty' => $data[1], + 'name' => $data[2], + 'width' => $data[3], 'length' => $data[4], - 'depth' => $data[5], + 'depth' => $data[5], 'weight' => $data[6], ]; } else { $tests[$data[0]] = [ - 'test' => $data[0], + 'test' => $data[0], 'boxes' => $boxes, 'items' => [ [ - 'qty' => $data[1], - 'name' => $data[2], - 'width' => $data[3], + 'qty' => $data[1], + 'name' => $data[2], + 'width' => $data[3], 'length' => $data[4], - 'depth' => $data[5], + 'depth' => $data[5], 'weight' => $data[6], ], ], - 'expected2D' => (int)$expected['2D'][$data[0]]['boxes'], - 'expected3D' => (int)$expected['3D'][$data[0]]['boxes'], - 'weightVariance2D' => $expected['2D'][$data[0]]['weightVariance'], - 'weightVariance3D' => $expected['3D'][$data[0]]['weightVariance'], + 'expected2D' => (int) $expected['2D'][$data[0]]['boxes'], + 'expected3D' => (int) $expected['3D'][$data[0]]['boxes'], + 'weightVariance2D' => $expected['2D'][$data[0]]['weightVariance'], + 'weightVariance3D' => $expected['3D'][$data[0]]['weightVariance'], 'volumeUtilisation2D' => $expected['2D'][$data[0]]['utilisation'], 'volumeUtilisation3D' => $expected['3D'][$data[0]]['utilisation'], ]; diff --git a/tests/ItemListTest.php b/tests/ItemListTest.php index 8f70d7db..a542a18a 100644 --- a/tests/ItemListTest.php +++ b/tests/ItemListTest.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use DVDoug\BoxPacker\Test\TestItem; diff --git a/tests/PackedBoxListTest.php b/tests/PackedBoxListTest.php index 39bda137..ee82df4a 100644 --- a/tests/PackedBoxListTest.php +++ b/tests/PackedBoxListTest.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use DVDoug\BoxPacker\Test\TestBox; diff --git a/tests/PackedBoxTest.php b/tests/PackedBoxTest.php index b05fa82a..fb32b56f 100644 --- a/tests/PackedBoxTest.php +++ b/tests/PackedBoxTest.php @@ -37,6 +37,7 @@ public function testGetters() self::assertEquals(34, $packedBox->getRemainingDepth()); self::assertEquals(2540, $packedBox->getRemainingWeight()); self::assertEquals(5445440, $packedBox->getInnerVolume()); + self::assertEquals($packedItemList, $packedBox->getPackedItems()); } /** diff --git a/tests/PackedItemTest.php b/tests/PackedItemTest.php new file mode 100644 index 00000000..01d9dcbe --- /dev/null +++ b/tests/PackedItemTest.php @@ -0,0 +1,25 @@ +getVolume()); + } +} diff --git a/tests/PackerTest.php b/tests/PackerTest.php index 5e38d623..c28fc122 100644 --- a/tests/PackerTest.php +++ b/tests/PackerTest.php @@ -4,7 +4,6 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; use DVDoug\BoxPacker\Test\TestBox; @@ -72,7 +71,7 @@ public function testWeightRedistributionActivatesOrNot() $packer = new Packer(); $packer->addBox(new TestBox('Box', 1, 1, 3, 0, 1, 1, 3, 3)); - $packer->addItem(new TestItem('Item', 1, 1, 1, 1, false), 4); + $packer->addItem(new TestItem('Item', 1, 1, 1, 1), 4); /** @var PackedBox[] $packedBoxes */ $packedBoxes = iterator_to_array($packer->pack(), false); @@ -83,7 +82,7 @@ public function testWeightRedistributionActivatesOrNot() // same items, but with redistribution turned off - expecting 3+1 based on pure fit $packer = new Packer(); $packer->addBox(new TestBox('Box', 1, 1, 3, 0, 1, 1, 3, 3)); - $packer->addItem(new TestItem('Item', 1, 1, 1, 1, false), 4); + $packer->addItem(new TestItem('Item', 1, 1, 1, 1), 4); $packer->setMaxBoxesToBalanceWeight(1); /** @var PackedBox[] $packedBoxes */ @@ -100,7 +99,7 @@ public function testIssue52A() { $packer = new Packer(); $packer->addBox(new TestBox('Box', 100, 50, 50, 0, 100, 50, 50, 5000)); - $packer->addItem(new TestItem('Item', 15, 13, 8, 407, true), 2); + $packer->addItem(new TestItem('Item', 15, 13, 8, 407), 2); $packedBoxes = $packer->pack(); self::assertCount(1, $packedBoxes); @@ -116,10 +115,10 @@ public function testIssue52B() { $packer = new Packer(); $packer->addBox(new TestBox('Box', 370, 375, 60, 140, 364, 374, 40, 3000)); - $packer->addItem(new TestItem('Item 1', 220, 310, 12, 679, true)); - $packer->addItem(new TestItem('Item 2', 210, 297, 11, 648, true)); - $packer->addItem(new TestItem('Item 3', 210, 297, 5, 187, true)); - $packer->addItem(new TestItem('Item 4', 148, 210, 32, 880, true)); + $packer->addItem(new TestItem('Item 1', 220, 310, 12, 679)); + $packer->addItem(new TestItem('Item 2', 210, 297, 11, 648)); + $packer->addItem(new TestItem('Item 3', 210, 297, 5, 187)); + $packer->addItem(new TestItem('Item 4', 148, 210, 32, 880)); $packedBoxes = $packer->pack(); self::assertCount(1, $packedBoxes); @@ -135,9 +134,9 @@ public function testIssue52C() { $packer = new Packer(); $packer->addBox(new TestBox('Box', 230, 300, 240, 160, 230, 300, 240, 15000)); - $packer->addItem(new TestItem('Item 1', 210, 297, 4, 213, true)); - $packer->addItem(new TestItem('Item 2', 80, 285, 70, 199, true)); - $packer->addItem(new TestItem('Item 3', 80, 285, 70, 199, true)); + $packer->addItem(new TestItem('Item 1', 210, 297, 4, 213)); + $packer->addItem(new TestItem('Item 2', 80, 285, 70, 199)); + $packer->addItem(new TestItem('Item 3', 80, 285, 70, 199)); /** @var PackedBox[] $packedBoxes */ $packedBoxes = iterator_to_array($packer->pack(), false); @@ -148,6 +147,22 @@ public function testIssue52C() self::assertEquals(74, $packedBoxes[0]->getUsedDepth()); } + /** + * Test case where last item algorithm picks a slightly inefficient box. + */ + public function testIssue117() + { + $packer = new Packer(); + $packer->addBox(new TestBox('Box A', 36, 8, 3, 0, 36, 8, 3, 2)); + $packer->addBox(new TestBox('Box B', 36, 8, 8, 0, 36, 8, 8, 2)); + $packer->addItem(new TestItem('Item 1', 35, 7, 2, 1)); + $packer->addItem(new TestItem('Item 2', 6, 5, 1, 1)); + /** @var PackedBox[] $packedBoxes */ + $packedBoxes = iterator_to_array($packer->pack(), false); + self::assertCount(1, $packedBoxes); + self::assertEquals('Box A', $packedBoxes[0]->getBox()->getReference()); + } + /** * Where 2 perfectly filled boxes are a choice, need to ensure we pick the larger one or there is a cascading * failure of many small boxes instead of a few larger ones. @@ -157,15 +172,47 @@ public function testIssue38() $packer = new Packer(); $packer->addBox(new TestBox('Box1', 2, 2, 2, 0, 2, 2, 2, 1000)); $packer->addBox(new TestBox('Box2', 4, 4, 4, 0, 4, 4, 4, 1000)); - $packer->addItem(new TestItem('Item 1', 1, 1, 1, 100, false)); - $packer->addItem(new TestItem('Item 2', 1, 1, 1, 100, false)); - $packer->addItem(new TestItem('Item 3', 1, 1, 1, 100, false)); - $packer->addItem(new TestItem('Item 4', 1, 1, 1, 100, false)); - $packer->addItem(new TestItem('Item 5', 2, 2, 2, 100, false)); - $packer->addItem(new TestItem('Item 6', 2, 2, 2, 100, false)); - $packer->addItem(new TestItem('Item 7', 2, 2, 2, 100, false)); - $packer->addItem(new TestItem('Item 8', 2, 2, 2, 100, false)); - $packer->addItem(new TestItem('Item 9', 4, 4, 4, 100, false)); + $packer->addItem(new TestItem('Item 1', 1, 1, 1, 100)); + $packer->addItem(new TestItem('Item 2', 1, 1, 1, 100)); + $packer->addItem(new TestItem('Item 3', 1, 1, 1, 100)); + $packer->addItem(new TestItem('Item 4', 1, 1, 1, 100)); + $packer->addItem(new TestItem('Item 5', 2, 2, 2, 100)); + $packer->addItem(new TestItem('Item 6', 2, 2, 2, 100)); + $packer->addItem(new TestItem('Item 7', 2, 2, 2, 100)); + $packer->addItem(new TestItem('Item 8', 2, 2, 2, 100)); + $packer->addItem(new TestItem('Item 9', 4, 4, 4, 100)); + + /** @var PackedBox[] $packedBoxes */ + $packedBoxes = iterator_to_array($packer->pack(), false); + + self::assertCount(2, $packedBoxes); + } + + /** + * From issue #168. + */ + public function testIssue168() + { + $packer = new Packer(); + $packer->addBox(new TestBox('Small', 85, 190, 230, 30, 85, 190, 230, 10000)); + $packer->addBox(new TestBox('Medium', 220, 160, 160, 50, 220, 160, 160, 10000)); + $packer->addItem(new TestItem('Item', 55, 85, 122, 350)); + + /** @var PackedBox[] $packedBoxes */ + $packedBoxes = iterator_to_array($packer->pack(), false); + + self::assertCount(1, $packedBoxes); + self::assertEquals('Small', $packedBoxes[0]->getBox()->getReference()); + } + + /** + * From issue #170. + */ + public function testIssue170() + { + $packer = new Packer(); + $packer->addBox(new TestBox('Box', 170, 120, 120, 2000, 170, 120, 120, 60000)); + $packer->addItem(new TestItem('Item', 70, 130, 2, 657), 100); /** @var PackedBox[] $packedBoxes */ $packedBoxes = iterator_to_array($packer->pack(), false); diff --git a/tests/Test/ConstrainedPlacementByCountTestItem.php b/tests/Test/ConstrainedPlacementByCountTestItem.php new file mode 100644 index 00000000..1f53b9db --- /dev/null +++ b/tests/Test/ConstrainedPlacementByCountTestItem.php @@ -0,0 +1,53 @@ + batteries per box. + * + * @param Box $box + * @param PackedItemList $alreadyPackedItems + * @param int $proposedX + * @param int $proposedY + * @param int $proposedZ + * @param int $width + * @param int $length + * @param int $depth + * @return bool + */ + public function canBePacked( + Box $box, + PackedItemList $alreadyPackedItems, + $proposedX, + $proposedY, + $proposedZ, + $width, + $length, + $depth + ) { + $alreadyPackedType = array_filter( + iterator_to_array($alreadyPackedItems, false), + function (PackedItem $item) { + return $item->getItem()->getDescription() === $this->getDescription(); + } + ); + + return count($alreadyPackedType) + 1 <= static::$limit; + } +} diff --git a/tests/Test/ConstrainedPlacementNoStackingTestItem.php b/tests/Test/ConstrainedPlacementNoStackingTestItem.php new file mode 100644 index 00000000..349f3578 --- /dev/null +++ b/tests/Test/ConstrainedPlacementNoStackingTestItem.php @@ -0,0 +1,58 @@ + batteries per box. + * + * @param Box $box + * @param PackedItemList $alreadyPackedItems + * @param int $proposedX + * @param int $proposedY + * @param int $proposedZ + * @param int $width + * @param int $length + * @param int $depth + * @return bool + */ + public function canBePacked( + Box $box, + PackedItemList $alreadyPackedItems, + $proposedX, + $proposedY, + $proposedZ, + $width, + $length, + $depth + ) { + $alreadyPackedType = array_filter( + iterator_to_array($alreadyPackedItems, false), + function (PackedItem $item) { + return $item->getItem()->getDescription() === $this->getDescription(); + } + ); + + /** @var PackedItem $alreadyPacked */ + foreach ($alreadyPackedType as $alreadyPacked) { + if ( + $alreadyPacked->getZ() + $alreadyPacked->getDepth() === $proposedZ && + $proposedX >= $alreadyPacked->getX() && $proposedX <= ($alreadyPacked->getX() + $alreadyPacked->getWidth()) && + $proposedY >= $alreadyPacked->getY() && $proposedY <= ($alreadyPacked->getY() + $alreadyPacked->getLength())) { + return false; + } + } + + return true; + } +} diff --git a/tests/Test/ConstrainedTestItem.php b/tests/Test/ConstrainedTestItem.php new file mode 100644 index 00000000..c1e0cd93 --- /dev/null +++ b/tests/Test/ConstrainedTestItem.php @@ -0,0 +1,38 @@ +getDescription() === $this->getDescription(); + } + ); + + return count($alreadyPackedType) + 1 <= static::$limit; + } +} diff --git a/tests/Test/TestBox.php b/tests/Test/TestBox.php index 66e55f55..685a9299 100644 --- a/tests/Test/TestBox.php +++ b/tests/Test/TestBox.php @@ -8,8 +8,9 @@ namespace DVDoug\BoxPacker\Test; use DVDoug\BoxPacker\Box; +use JsonSerializable; -class TestBox implements Box +class TestBox implements Box, JsonSerializable { /** * @var string @@ -176,4 +177,19 @@ public function getMaxWeight() { return $this->maxWeight; } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return [ + 'reference' => $this->reference, + 'innerWidth' => $this->innerWidth, + 'innerLength' => $this->innerLength, + 'innerDepth' => $this->innerDepth, + 'emptyWeight' => $this->emptyWeight, + 'maxWeight' => $this->maxWeight, + ]; + } } diff --git a/tests/Test/TestItem.php b/tests/Test/TestItem.php index 0a5e483c..7c5789bd 100644 --- a/tests/Test/TestItem.php +++ b/tests/Test/TestItem.php @@ -8,9 +8,10 @@ namespace DVDoug\BoxPacker\Test; use DVDoug\BoxPacker\Item; +use JsonSerializable; use stdClass; -class TestItem implements Item +class TestItem implements Item, JsonSerializable { /** * @var string @@ -131,4 +132,18 @@ public function getVolume() { return $this->volume; } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() + { + return [ + 'description' => $this->description, + 'width' => $this->width, + 'length' => $this->length, + 'depth' => $this->depth, + 'weight' => $this->weight + ]; + } } diff --git a/tests/VolumePackerTest.php b/tests/VolumePackerTest.php index 1d6e9542..5e4e74bc 100644 --- a/tests/VolumePackerTest.php +++ b/tests/VolumePackerTest.php @@ -4,11 +4,12 @@ * * @author Doug Wright */ - namespace DVDoug\BoxPacker; +use DVDoug\BoxPacker\Test\ConstrainedPlacementByCountTestItem; +use DVDoug\BoxPacker\Test\ConstrainedPlacementNoStackingTestItem; use DVDoug\BoxPacker\Test\TestBox; -use DVDoug\BoxPacker\Test\TestConstrainedTestItem; +use DVDoug\BoxPacker\Test\ConstrainedTestItem; use DVDoug\BoxPacker\Test\TestItem; use PHPUnit\Framework\TestCase; @@ -24,11 +25,11 @@ public function testUsedDimensionsCalculatedCorrectly() { $box = new TestBox('Bundle', 75, 15, 15, 0, 75, 15, 15, 30); $itemList = new ItemList(); - $itemList->insert(new TestItem('Item 1', 14, 12, 2, 2, true)); - $itemList->insert(new TestItem('Item 2', 14, 12, 2, 2, true)); - $itemList->insert(new TestItem('Item 3', 14, 12, 2, 2, true)); - $itemList->insert(new TestItem('Item 4', 14, 12, 2, 2, true)); - $itemList->insert(new TestItem('Item 5', 14, 12, 2, 2, true)); + $itemList->insert(new TestItem('Item 1', 14, 12, 2, 2)); + $itemList->insert(new TestItem('Item 2', 14, 12, 2, 2)); + $itemList->insert(new TestItem('Item 3', 14, 12, 2, 2)); + $itemList->insert(new TestItem('Item 4', 14, 12, 2, 2)); + $itemList->insert(new TestItem('Item 5', 14, 12, 2, 2)); $packer = new VolumePacker($box, $itemList); $packedBox = $packer->pack(); @@ -45,9 +46,9 @@ public function testUsedWidthAndRemainingWidthHandleRotationsCorrectly() { $packer = new Packer(); $packer->addBox(new TestBox('Box', 23, 27, 14, 0, 23, 27, 14, 30)); - $packer->addItem(new TestItem('Item 1', 11, 22, 2, 1, true), 3); - $packer->addItem(new TestItem('Item 2', 11, 22, 2, 1, true), 4); - $packer->addItem(new TestItem('Item 3', 6, 17, 2, 1, true), 3); + $packer->addItem(new TestItem('Item 1', 11, 22, 2, 1), 3); + $packer->addItem(new TestItem('Item 2', 11, 22, 2, 1), 4); + $packer->addItem(new TestItem('Item 3', 6, 17, 2, 1), 3); $packedBoxes = $packer->pack(); self::assertCount(1, $packedBoxes); @@ -65,27 +66,74 @@ public function testUsedWidthAndRemainingWidthHandleRotationsCorrectly() /** * Test that constraint handling works correctly. */ - public function testConstraints() + public function testLegacyConstraints() { // first a regular item $packer = new Packer(); $packer->addBox(new TestBox('Box', 10, 10, 10, 0, 10, 10, 10, 0)); - $packer->addItem(new TestItem('Item', 1, 1, 1, 0, false), 8); + $packer->addItem(new TestItem('Item', 1, 1, 1, 0), 8); $packedBoxes = $packer->pack(); self::assertCount(1, $packedBoxes); // same dimensions but now constrained by type - TestConstrainedTestItem::$limit = 2; + ConstrainedTestItem::$limit = 2; $packer = new Packer(); $packer->addBox(new TestBox('Box', 10, 10, 10, 0, 10, 10, 10, 0)); - $packer->addItem(new TestConstrainedTestItem('Item', 1, 1, 1, 0, false), 8); + $packer->addItem(new ConstrainedTestItem('Item', 1, 1, 1, 0), 8); $packedBoxes = $packer->pack(); self::assertCount(4, $packedBoxes); } + /** + * Test that constraint handling works correctly. + */ + public function testNewConstraintMatchesLegacy() + { + // first a regular item + $packer = new Packer(); + $packer->addBox(new TestBox('Box', 10, 10, 10, 0, 10, 10, 10, 0)); + $packer->addItem(new TestItem('Item', 1, 1, 1, 0), 8); + $packedBoxes = $packer->pack(); + + self::assertCount(1, $packedBoxes); + + // same dimensions but now constrained by type + ConstrainedPlacementByCountTestItem::$limit = 2; + + $packer = new Packer(); + $packer->addBox(new TestBox('Box', 10, 10, 10, 0, 10, 10, 10, 0)); + $packer->addItem(new ConstrainedPlacementByCountTestItem('Item', 1, 1, 1, 0), 8); + $packedBoxes = $packer->pack(); + + self::assertCount(4, $packedBoxes); + } + + /** + * Test that constraint handling works correctly. + */ + public function testNewConstraint() + { + // first a regular item + $packer = new Packer(); + $packer->addBox(new TestBox('Box', 4, 1, 2, 0, 4, 1, 2, 0)); + $packer->addItem(new TestItem('Item', 1, 1, 1, 0), 8); + $packedBoxes = $packer->pack(); + + self::assertCount(1, $packedBoxes); + + // same dimensions but now constrained to not have stacking + + $packer = new Packer(); + $packer->addBox(new TestBox('Box', 4, 1, 2, 0, 4, 1, 2, 0)); + $packer->addItem(new ConstrainedPlacementNoStackingTestItem('Item', 1, 1, 1, 0), 8); + $packedBoxes = $packer->pack(); + + self::assertCount(2, $packedBoxes); + } + /** * Test an infinite loop doesn't come back. */ @@ -93,10 +141,10 @@ public function testIssue14() { $packer = new Packer(); $packer->addBox(new TestBox('29x1x23Box', 29, 1, 23, 0, 29, 1, 23, 100)); - $packer->addItem(new TestItem('13x1x10Item', 13, 1, 10, 1, true)); - $packer->addItem(new TestItem('9x1x6Item', 9, 1, 6, 1, true)); - $packer->addItem(new TestItem('9x1x6Item', 9, 1, 6, 1, true)); - $packer->addItem(new TestItem('9x1x6Item', 9, 1, 6, 1, true)); + $packer->addItem(new TestItem('13x1x10Item', 13, 1, 10, 1)); + $packer->addItem(new TestItem('9x1x6Item', 9, 1, 6, 1)); + $packer->addItem(new TestItem('9x1x6Item', 9, 1, 6, 1)); + $packer->addItem(new TestItem('9x1x6Item', 9, 1, 6, 1)); $packedBoxes = $packer->pack(); self::assertCount(1, $packedBoxes); @@ -110,7 +158,7 @@ public function testIssue47A() $box = new TestBox('165x225x25Box', 165, 225, 25, 0, 165, 225, 25, 100); $item = new TestItem('20x69x20Item', 20, 69, 20, 0); $itemList = new ItemList(); - for ($i = 0; $i < 23; $i++) { + for ($i = 0; $i < 23; ++$i) { $itemList->insert($item); } @@ -128,7 +176,7 @@ public function testIssue47B() $box = new TestBox('165x225x25Box', 165, 225, 25, 0, 165, 225, 25, 100); $item = new TestItem('20x69x20Item', 69, 20, 20, 0); $itemList = new ItemList(); - for ($i = 0; $i < 23; $i++) { + for ($i = 0; $i < 23; ++$i) { $itemList->insert($item); } @@ -147,7 +195,7 @@ public function testAllowsRotatedBoxesInNewRow() $box = new TestBox('40x70x30InternalBox', 40, 70, 30, 0, 40, 70, 30, 1000); $item = new TestItem('30x10x30item', 30, 10, 30, 0); $itemList = new ItemList(); - for ($i = 0; $i < 9; $i++) { + for ($i = 0; $i < 9; ++$i) { $itemList->insert($item); } @@ -157,15 +205,45 @@ public function testAllowsRotatedBoxesInNewRow() self::assertCount(9, $packedBox->getItems()); } + /** + * From issue #124. + */ + public function testUnpackedSpaceInsideLayersIsFilled() + { + $this->markTestSkipped(); // until bug is fixed + + $box = new TestBox('Box', 4, 14, 11, 0, 4, 14, 11, 100); + $itemList = new ItemList(); + $itemList->insert(new TestItem('Item 1', 8, 8, 2, 1)); + $itemList->insert(new TestItem('Item 2', 4, 4, 4, 1)); + $itemList->insert(new TestItem('Item 3', 4, 4, 4, 1)); + + $packer = new VolumePacker($box, $itemList); + $packedBox = $packer->pack(); + + self::assertCount(3, $packedBox->getItems()); + + $box = new TestBox('Box', 14, 11, 4, 0, 14, 11, 4, 100); + $itemList = new ItemList(); + $itemList->insert(new TestItem('Item 1', 8, 8, 2, 1)); + $itemList->insert(new TestItem('Item 2', 4, 4, 4, 1)); + $itemList->insert(new TestItem('Item 3', 4, 4, 4, 1)); + + $packer = new VolumePacker($box, $itemList); + $packedBox = $packer->pack(); + + self::assertCount(3, $packedBox->getItems()); + } + /** * Test stability of items is calculated appropriately. */ public function testIssue148() { $box = new TestBox('Box', 27, 37, 22, 100, 25, 36, 21, 15000); - $item = new TestItem('Item', 6, 12, 20, 100, false); + $item = new TestItem('Item', 6, 12, 20, 100); $itemList = new ItemList(); - for ($i = 0; $i < 12; $i++) { + for ($i = 0; $i < 12; ++$i) { $itemList->insert($item); } @@ -175,9 +253,9 @@ public function testIssue148() self::assertCount(12, $packedBox->getItems()); $box = new TestBox('Box', 27, 37, 22, 100, 25, 36, 21, 15000); - $item = new TestItem('Item', 6, 12, 20, 100, true); + $item = new TestItem('Item', 6, 12, 20, 100); $itemList = new ItemList(); - for ($i = 0; $i < 12; $i++) { + for ($i = 0; $i < 12; ++$i) { $itemList->insert($item); } @@ -195,7 +273,8 @@ public function testIssue147A() $box = new TestBox('Box', 250, 1360, 260, 0, 250, 1360, 260, 30000); $itemList = new ItemList(); $item = new TestItem('Item', 90, 200, 200, 150); - for ($i = 0; $i < 14; $i++) { + + for ($i = 0; $i < 14; ++$i) { $itemList->insert($item); } $packer = new VolumePacker($box, $itemList); @@ -203,4 +282,28 @@ public function testIssue147A() self::assertCount(14, $packedBox->getItems()); } + + /** + * From issue #164. + */ + public function testIssue164() + { + $box = new TestBox('Box', 820, 820, 830, 0, 820, 820, 830, 10000); + + $itemList = new ItemList(); + $itemList->insert(new TestItem('Item 1', 110, 110, 50, 100)); + $itemList->insert(new TestItem('Item 2', 100, 300, 30, 100)); + $itemList->insert(new TestItem('Item 3', 100, 150, 50, 100)); + $itemList->insert(new TestItem('Item 4', 100, 200, 80, 110)); + $itemList->insert(new TestItem('Item 5', 80, 150, 80, 50)); + $itemList->insert(new TestItem('Item 6', 80, 150, 80, 50)); + $itemList->insert(new TestItem('Item 7', 80, 150, 80, 50)); + $itemList->insert(new TestItem('Item 8', 270, 70, 60, 350)); + $itemList->insert(new TestItem('Item 9', 150, 150, 80, 180)); + $itemList->insert(new TestItem('Item 10', 80, 150, 80, 50)); + + $packer = new VolumePacker($box, $itemList); + $packedBox = $packer->pack(); + self::assertCount(10, $packedBox->getItems()); + } } diff --git a/tests/data/expected.csv b/tests/data/expected.csv index b50e5c93..1761197f 100644 --- a/tests/data/expected.csv +++ b/tests/data/expected.csv @@ -178,7 +178,7 @@ 09b141fd18a5af271ba1efa2196e0267,1,0,29.1,1,0,29.1 09c5b203bf83f133b4013c3d2991fec6,2,2500,27.8,1,0,8.3 09d84d2dff8d25e8cbb8f63ac1dbcfe4,1,0,33.1,1,0,33.1 -09dcfda35a6149fa4baea1fda1788198,2,11130.3,31,1,0,9.3 +09dcfda35a6149fa4baea1fda1788198,2,11130.3,31,2,5550.3,31 09e2db5a9b80b82937a139f3105fbf8e,2,4761,37.2,1,0,11.2 09ed14c38d2cd48d0d0d97ccb2098ea0,1,0,48.2,1,0,48.2 09efccb17da07ece808dbfa7687db577,1,0,23.1,1,0,23.1 @@ -545,7 +545,7 @@ 1df392d16403002997b44e622b40c25d,1,0,30.2,1,0,30.2 1df7261c9a68c7aac4557b3ef1c92689,2,1764,56.7,2,1764,56.7 1e2ea4827764957f2f0ebb6fc234b5a8,1,0,27.9,1,0,27.9 -1e3576b097febae61d71b70ae74c1c91,6,2099520,62.9,2,894916,37.9 +1e3576b097febae61d71b70ae74c1c91,6,2099520,62.9,2,1521,37.9 1e52311b44caf51b40cfe59ecb1142f4,2,25760.3,24.7,1,0,7.4 1e541545aa9d21512203412de01518d6,1,0,33,1,0,33 1e621fccb032a07c9aac966967279a8b,2,18090.3,35.7,1,0,10.7 @@ -570,7 +570,7 @@ 1f4f44a4e5664e628d566dec2ded0aa3,1,0,45.2,1,0,45.2 1f51dea652b19ce9db60b12dfb1b58c6,1,0,42.7,1,0,42.7 1f706b3f264b93faa5d9a7918deb4456,2,37442.3,39.7,1,0,11.9 -1f87bf154d63455989b352ea94b3dfcd,2,18906.3,27.9,1,0,16.9 +1f87bf154d63455989b352ea94b3dfcd,2,18906.3,27.9,2,1722.3,14.7 1f8c9133a6e79c41924becae0695988a,1,0,26.4,1,0,26.4 1f98f12faf5a5cd32adf0b59d34309b7,1,0,41.3,1,0,41.3 1f9bc3968e53725db6175dac9f8e5a50,1,0,32.4,1,0,32.4 @@ -868,7 +868,7 @@ 3223ff53874ff3768881db6e163c96a3,2,99856,31.5,1,0,19.1 323d3a61ac8f60761edc56c34b339a1d,1,0,40.8,1,0,40.8 3242e661790ad530e5a8419a5c7c0b5b,1,0,22.8,1,0,22.8 -3247e39c4bfd94e9f525f7ef5c179317,2,366025,36.9,2,1,21.2 +3247e39c4bfd94e9f525f7ef5c179317,2,403225,70.1,2,1764,29.2 326b6af0e5d3ac4f72622ce744ee4b27,1,0,32.4,1,0,32.4 3276e0aa9afede0f2e5b0d5e3dcf7426,2,14884,23.9,1,0,7.2 328f28aadcd871aa3ade0646fda55b85,1,0,25.2,1,0,25.2 @@ -1037,7 +1037,7 @@ 3bd6564e4bffc131afe76760e02db797,3,64910.2,36.7,1,0,16.5 3bda69c3fe4e30d6944a02d696661fe3,1,0,29.2,1,0,29.2 3bdea76452dfd0a2dd349e05688ceea7,1,0,36.2,1,0,36.2 -3bec6daa3c92aab45d0db63b688bad9e,5,89806.4,34.3,2,79806.3,18.1 +3bec6daa3c92aab45d0db63b688bad9e,5,89806.4,34.3,2,3543806.3,18.1 3becb246809835f192b15039849f3528,1,0,27.5,1,0,27.5 3bee0a82a9098c92945daac991fca070,2,12.3,23.7,1,0,7.1 3c1b74fc52f870503d59c3327da6d203,2,607620.3,40,1,0,24.2 @@ -1045,7 +1045,7 @@ 3c2bc39429c8d1a0db6369149f454d65,2,2916,39.5,1,0,11.9 3c47276e13a1bf073b7f2987f09b21d9,1,0,48,1,0,48 3c4f92d98c4e7792e46c0269daab7d6d,2,400,28.5,1,0,8.5 -3c51cd3b2b4b213f076ba27e90d04225,3,6253450.7,48.3,2,3218436,25.1 +3c51cd3b2b4b213f076ba27e90d04225,3,6253450.7,48.3,2,835396,25.1 3c583eb716f36fa0ebe89e3573c733c0,1,0,28.5,1,0,28.5 3c58eba0522266abecca9b20aad46677,1,0,17.5,1,0,17.5 3c63070dfe31a39510ed0fdc001cd339,1,0,41.2,1,0,41.2 @@ -1123,7 +1123,7 @@ 410cac71760b16a88bb0dbaf88a4d996,1,0,28.6,1,0,28.6 41251af4ab36c9b1c4c4c0e86932beb1,1,0,40.6,1,0,40.6 412d2129092abe01690fe2547274aa96,1,0,19.2,1,0,19.2 -41318da1afa1a3298f046302bbca5222,3,29866.7,34,2,40000,7.6 +41318da1afa1a3298f046302bbca5222,3,29866.7,34,1,0,15.3 413c19c6e0a23d66962980f3c4e2e145,2,12.3,24.3,2,12.3,24.3 414b9c41bf7b28d67fbf36392318955b,2,676,31.3,1,0,9.4 414fe91e92f8ee2b9da5bd53947e8602,1,0,39.3,1,0,39.3 @@ -1195,7 +1195,7 @@ 44ff4fe711acb48be3ca84d0c0c8c351,1,0,38.9,1,0,38.9 45153f16dc4fcf9a51d4f45156c3bb29,2,113569,51.8,2,113569,51.8 4536a8f136729440b4762de771b72c45,2,870.3,41.5,1,0,12.4 -4541ef924dcadbd4118b9521b1ec7981,2,4458432.3,29.7,1,0,34.2 +4541ef924dcadbd4118b9521b1ec7981,2,3715256.3,29.7,1,0,34.2 4543031646f0dace54dd007c834e3581,1,0,31.1,1,0,31.1 45499743a72c9cff765b00fdb3e3f6df,1,0,18.6,1,0,18.6 455ca7f6c94ffd38cb8af2339d546e33,1,0,30.2,1,0,30.2 @@ -1317,7 +1317,7 @@ 4bbb8da8563889feed5727ac8f332eda,1,0,37.9,1,0,37.9 4be1d10c7f69cc8fdf77ea49fb08b183,1,0,25.6,1,0,25.6 4c039757edba47e82c1d95ed499896d6,2,17556.3,30,1,0,9 -4c2d6c9bdcfdd2aaac599d91c9192dee,2,333506.3,32.4,2,3660.3,9.8 +4c2d6c9bdcfdd2aaac599d91c9192dee,2,333506.3,32.4,1,0,19.7 4c3bda3a0021a8c42b66bf989df6b7a8,1,0,60.1,1,0,60.1 4c4ea6ecfb63e37142e5cc4ed9793442,1,0,61,1,0,61 4c5362ea0d360523704c0a29e02c4a53,1,0,25.3,1,0,25.3 @@ -1466,7 +1466,7 @@ 547b3c46daded352fbf63c857d87921c,6,8937399.5,61.4,2,167690.3,50.8 548c715873b6223fe988a0f94c932b05,1,0,57.2,1,0,57.2 549e072fe0adbf86a9cdf96456702e6b,2,58806.3,26.5,1,0,8 -54aa5358f035a72d895437e03cf3393c,1,0,51.2,1,0,51.2 +54aa5358f035a72d895437e03cf3393c,2,121,35.2,1,0,51.2 54d00e9d39e00973e74abb7d463509df,2,3782.3,43.7,1,0,13.1 54d7241c191e5c5d30f55f86f3c2a1e2,2,25600,37.6,1,0,11.3 54ebd8d18b8e5eed18b5f0e263dc6449,2,9,36.5,1,0,10.9 @@ -1608,7 +1608,7 @@ 5cdf36d57dca1decf25b21fc70ccb7b4,2,3136,32.4,1,0,9.7 5ce2156cbcd2167998a9f5b143b44094,1,0,15.8,1,0,15.8 5d00aedb31357d336f4d8345f2dfbba4,1,0,5.7,1,0,5.7 -5d25acff5712b8a133c693ea5951b005,2,1300740.3,38.8,2,1300740.3,38.8 +5d25acff5712b8a133c693ea5951b005,2,1300740.3,38.8,2,132.3,11.8 5d32bc595150184de9f7f0b2115c5310,1,0,27.9,1,0,27.9 5d32fc5743aee3b38c57715bd1a2f99e,2,144,29.9,1,0,9 5d54995022690b8d50acb4c80a3e33f3,1,0,25.4,1,0,25.4 @@ -2002,7 +2002,7 @@ 73629c8e0e3c78d24121d25bfe5254f2,1,0,46.1,1,0,46.1 7366285d837921c76989d9f797c1f0c7,2,3364,37.1,1,0,11.1 736ba758ea3bddea61655031fcac9620,2,1600,47,1,0,14.1 -736f6ef9b25b16508d1c8c85417b15da,4,87372,40.5,2,266256,12.1 +736f6ef9b25b16508d1c8c85417b15da,4,87372,40.5,2,38416,12.1 739584d7aee4c547320e35e357da4457,1,0,39.5,1,0,39.5 73aa7fec8faa51621fc3ac992c549816,1,0,8.6,1,0,8.6 73bb40c2188f99cde2e89bfe4af1c7f8,1,0,30.5,1,0,30.5 @@ -2076,7 +2076,7 @@ 77ee435e81a6ed974d2d23d5ad92d758,1,0,43.8,1,0,43.8 77f36d03b8b0972966f784ede18df036,1,0,31.8,1,0,31.8 7803029edad1118c42361b90838021ff,1,0,15.5,1,0,15.5 -7808268fc06dc358047e06d04131dae6,2,121,40,2,121,40 +7808268fc06dc358047e06d04131dae6,2,34225,55,2,121,40 780e3fcb605b33a5293dc71d295e4f38,1,0,34.7,1,0,34.7 7818ebbfce480f11d9c91b9fdfeae9d7,1,0,33.9,1,0,33.9 78202b56af66040afa4b19d23164168b,1,0,18.9,1,0,18.9 @@ -2306,7 +2306,7 @@ 865c43072c811009f4da22ee0525dd8a,1,0,35.5,1,0,35.5 86662df0ffaffef90bc3a5148f5e2de8,1,0,53.3,1,0,53.3 8672f30b07050392198c25cf8ab568fd,2,6241,24.9,1,0,15.1 -867cd61bae65718bcc20e210c6b2234e,2,324,38.9,2,25,53.4 +867cd61bae65718bcc20e210c6b2234e,2,324,38.9,1,0,77.8 867f7b009007b84c4f8882a07f9a4d55,2,20449,27.2,2,20449,27.2 868c30a0d203e41ffa05cb3b1810bb7c,6,516.8,22,1,0,43.4 869253e021bfbdfc79ffb385cf2678ec,2,1234321,16.5,1,0,19 @@ -2638,7 +2638,7 @@ 9a9f177fa8fed3003de712310da1d068,1,0,35.4,1,0,35.4 9abd92f2d09b53f689cee54ea73449cb,1,0,46.9,1,0,46.9 9ad05f21be39341f42bfede52ef353c8,1,0,24.1,1,0,24.1 -9ad068429596a5f0f7fcfaf327f876a3,3,21147.6,24.1,2,5625,15.8 +9ad068429596a5f0f7fcfaf327f876a3,3,21147.6,24.1,2,14161,9.1 9ad36f87de2203f7e5ea72f59d798e4f,4,11470005.3,67.2,3,15678933.6,50.7 9add3fe8175b23cb776a00613b14924d,2,24492.3,21,1,0,12.7 9af9b9ac98cfaf7cc7bd36790a631ad8,1,0,37.5,1,0,37.5 @@ -2656,7 +2656,7 @@ 9b95269f0d32cfef0f5dd405766c9a63,1,0,33.7,1,0,33.7 9b96c4dbcd958014502327686e9ab3e3,3,36270.2,20.5,1,0,15.5 9bb9473afa3fd3d54ffc0eeffdb55d30,1,0,31,1,0,31 -9bf16c77adce8bf9e538f783cee3b23d,2,9,38.9,2,1369,38.9 +9bf16c77adce8bf9e538f783cee3b23d,2,9,38.9,2,3969,38.9 9c0bd0b0ad2027fb49a586fc1619806e,1,0,47.3,1,0,47.3 9c1acca34975b711d86acc93f070527f,1,0,16.9,1,0,16.9 9c28a01c3a87497f32d2c0220e0794a8,1,0,34.1,1,0,34.1 @@ -2911,7 +2911,7 @@ ab319eb0473abbe517e52ead26007e9d,2,42230.3,47.1,2,42230.3,47.1 ab3b1c260096541d4b6b2c0039fe28b6,2,6006.3,24.4,1,0,14.8 ab47749b81ef91adb192c8e12dbc70c4,1,0,5,1,0,5 ab51f10fdd2a474f6764d802cb5da2eb,1,0,16.2,1,0,16.2 -ab555bbbd49151f8ba54ce0909d32075,4,12687614.3,58.2,2,164836,48.5 +ab555bbbd49151f8ba54ce0909d32075,4,12687614.3,58.2,2,178929,48.5 ab5bf7d5840fe7fa56529f666e09f15f,2,30800.3,28,1,0,8.4 ab5d89243b28752cd4715679a536217e,1,0,20.9,1,0,20.9 ab744fec3cae04e3bebce28e248899e4,2,81,32.1,1,0,9.6 @@ -2997,7 +2997,7 @@ af88b086b028d3c972dc3a63d73d14bf,1,0,25.6,1,0,25.6 af9ce79a63306b9afe8e298058368012,1,0,29.9,1,0,29.9 afcf3d02d04c4e14f20ee9206eb773f3,2,2040612.3,36,1,0,21.8 afd340fd1db50f62cea8fe8219f330fc,1,0,9.2,1,0,9.2 -afdc1e5ccd7cb344fe3e2d6395896c26,2,2844282.3,37.1,2,2844282.3,37.1 +afdc1e5ccd7cb344fe3e2d6395896c26,2,2844282.3,37.1,2,72.3,11.2 afe7ffb1f0c596924fab2a3d3cd3bca3,2,124962.3,28.7,2,124962.3,28.7 afe82686f800749adfc6eb13778a6bb8,1,0,41.2,1,0,41.2 afea3328620147789d47b433c86fde2c,1,0,36.9,1,0,36.9 @@ -3200,7 +3200,7 @@ bc2497a0d4639c3245e5480a1079db81,1,0,23.1,1,0,23.1 bc304f0347ad62af107d7cddf6747fdd,2,0,26.5,1,0,8 bc46fd29b4646612fe0ebc7d42f73c76,1,0,19.6,1,0,19.6 bc4732fabc753ffbc1c25dc195476a60,1,0,30.1,1,0,30.1 -bc5433842f60c54256dbea595e5332b5,1,0,13.8,1,0,13.8 +bc5433842f60c54256dbea595e5332b5,1,0,42,1,0,42 bc6c22d23caa4cfa4f9c1ec186ecb464,2,2550.3,26.4,1,0,7.9 bc7570dff0ab21af870017f8be25282f,2,2256.3,37.6,1,0,11.3 bc7d15157115094cb23160d4df6dafd4,1,0,67.8,1,0,67.8 @@ -3553,7 +3553,7 @@ d0efe6d5522001320a4372da93769920,1,0,33.1,1,0,33.1 d10e78eff303de831aff0a556a536a0f,1,0,48.7,1,0,48.7 d11076dc6cd4720a1fe9add55d2281aa,1,0,14.9,1,0,14.9 d1239d721e49347c9804bed0a6db56ab,1,0,39.3,1,0,39.3 -d12f9cb4e8c278ab3286ae03ea30313a,2,90000,23.3,1,0,7 +d12f9cb4e8c278ab3286ae03ea30313a,2,39204,11.5,1,0,7 d130d596db644f016ca8529c8abf46cc,1,0,37.8,1,0,37.8 d138dfe2bd5abdd66b951e505e6ce634,1,0,21.3,1,0,21.3 d144251f58433c0af02fa1a09ce64d30,1,0,74.6,1,0,74.6 @@ -3597,7 +3597,7 @@ d3d3ac90213ec9a8b659ea678866678f,2,95790.3,26.7,2,95790.3,26.7 d3e162ccd42cceda49e15bab20b0a14a,1,0,37.7,1,0,37.7 d3f293cc1263d2f81fc45fb78ec9bcbb,1,0,30.3,1,0,30.3 d3f48514d7c8d253cf5a2706f9827125,1,0,22.7,1,0,22.7 -d3fdc38ef5f3f8d537ff13f056cccd35,2,28730.3,35,2,4830.3,9.1 +d3fdc38ef5f3f8d537ff13f056cccd35,2,28730.3,35,1,0,10.5 d403c37d53c5df5f3d5aa1d3ce44bcdd,1,0,36.3,1,0,36.3 d417a6e1758a7e321461881924f8e4b1,1,0,20.7,1,0,20.7 d4186796e44b7902b7e6f54c0d7afddb,2,4761,34.4,1,0,10.3 @@ -3926,7 +3926,7 @@ e7ce49052d1b481395fc314783cc4432,2,35344,58.2,2,35344,58.2 e7ced8ad83c78b7444f9e435215eb123,1,0,15.8,1,0,15.8 e7d547f4a27f1cc9e2deae64bffe6acf,2,30625,34.7,2,30625,34.7 e7db60ae5af75bcf3a396dd309a7f562,1,0,27.9,1,0,27.9 -e7e2c807709067d472e42218a39c1ea1,2,304704,41.4,2,0,12.6 +e7e2c807709067d472e42218a39c1ea1,2,304704,41.4,1,0,25.1 e7e66b3751235216c9203c541cd6a42f,1,0,25.4,1,0,25.4 e7f242ddaa1e48789d42ad6fa0761cf8,2,342.3,35.8,1,0,10.7 e8039b14de52cf6e2ed80efe944dbd56,1,0,25.4,1,0,25.4 @@ -4274,7 +4274,7 @@ ff1304f1d9c4d48370a9e8493b82ac72,1,0,17.7,1,0,17.7 ff1840fa436207a10de43cbc72a23758,2,289,41.9,1,0,12.6 ff1cf5b77f5954f694f4fad7dc32618a,1,0,5.4,1,0,5.4 ff25a3d0f76c5f77cb049b894da0dd3d,1,0,48.5,1,0,48.5 -ff2a3cdc18fd0508d27ee9bd9fa25cc0,3,465174.9,35.4,2,992.3,18.4 +ff2a3cdc18fd0508d27ee9bd9fa25cc0,3,465174.9,35.4,1,0,26.8 ff53dad8ae0048b61c85a12051bff323,1,0,15.6,1,0,15.6 ff551458dc59f34a428baef330afb285,1,0,27.8,1,0,27.8 ff5ac2e5b4a3bc28a910eead81196f0c,1,0,48.3,1,0,48.3 @@ -4282,7 +4282,7 @@ ff6be5226619bba2551036a0dac99290,1,0,20.6,1,0,20.6 ff8faaa392d74a080d9a44a41ac79ff0,1,0,16.7,1,0,16.7 ff9a02c4a580d84e54798bbac348ebca,2,1806.3,11.3,1,0,6.9 ffa1c4171acd2d3d369b94f24ab650a2,3,1353088.9,41.1,2,5776,21.4 -ffcba0d0b8df978c07f42428a20c9be4,2,169,35,2,8649,35 +ffcba0d0b8df978c07f42428a20c9be4,2,169,35,1,0,10.5 ffd9461983351399dfedaae7bcadc965,1,0,27.5,1,0,27.5 ffe8f1bc3fd5947736f4f190cb0b6587,1,0,54.8,1,0,54.8 fff4498d84d151692c0c2c26d65e8044,1,0,19.5,1,0,19.5