com.github.skjolberg.packing.LargestAreaFitFirstPackager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of 3d-bin-container-packing Show documentation
Show all versions of 3d-bin-container-packing Show documentation
Library for 3D rectangular bin packing
package com.github.skjolberg.packing;
import com.github.skjolberg.packing.impl.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
/**
* Fit boxes into container, i.e. perform bin packing to a single container.
*
* Thread-safe implementation. The input Boxes must however only be used in a single thread at a time.
*/
public class LargestAreaFitFirstPackager extends Packager {
private final boolean footprintFirst;
/**
* Constructor
*
* @param containers list of containers
*/
public LargestAreaFitFirstPackager(List containers) {
this(containers, true, true, true);
}
/**
* Constructor
*
* @param containers list of containers
* @param footprintFirst start with box which has the largest footprint. If not, the highest box is first.
* @param rotate3D whether boxes can be rotated in all three directions (two directions otherwise)
* @param binarySearch if true, the packager attempts to find the best box given a binary search. Upon finding a container that can hold the boxes, given time, it also tries to find a better match.
*/
public LargestAreaFitFirstPackager(List containers, boolean rotate3D, boolean footprintFirst, boolean binarySearch) {
super(containers, rotate3D, binarySearch);
this.footprintFirst = footprintFirst;
}
/**
*
* Return a container which holds all the boxes in the argument
*
* @param containerProducts list of boxes to fit in a container.
* @param targetContainer the container to fit within
* @param deadline the system time in milliseconds at which the search should be aborted
* @return null if no match, or deadline reached
*/
public LAFFResult pack(List containerProducts, Container targetContainer, long deadline) {
return pack(containerProducts, targetContainer, deadLinePredicate(deadline));
}
public LAFFResult pack(List containerProducts, Container targetContainer, long deadline, AtomicBoolean interrupt) {
return pack(containerProducts, targetContainer, () -> deadlineReached(deadline) || interrupt.get());
}
public LAFFResult pack(List containerProducts, Container targetContainer, BooleanSupplier interrupt) {
Container holder = new Container(targetContainer);
Dimension freeSpace = targetContainer;
while(!containerProducts.isEmpty()) {
if(interrupt.getAsBoolean()) {
// fit2d below might have returned due to deadline
return null;
}
// choose the box with the largest surface area, that fits
// if the same then the one with minimum height
int currentIndex = getBestBox(holder, freeSpace, containerProducts);
if(currentIndex == -1) {
break;
}
Box currentBox = containerProducts.get(currentIndex);
// current box should have the optimal orientation already
// create a space which holds the full level
Space levelSpace = new Space(
targetContainer.getWidth(),
targetContainer.getDepth(),
currentBox.getHeight(),
0,
0,
holder.getStackHeight()
);
holder.addLevel();
containerProducts.remove(currentIndex);
if(!fit2D(containerProducts, holder, currentBox, levelSpace, interrupt)) {
return null;
}
freeSpace = holder.getFreeSpace();
}
return new LAFFResult(containerProducts, holder);
}
protected int getBestBox(Container holder, Dimension freeSpace, List containerProducts) {
// use a special case for boxes with full height
int currentIndex = -1;
boolean fullHeight = false;
for (int i = 0; i < containerProducts.size(); i++) {
Box box = containerProducts.get(i);
boolean fits;
if(rotate3D) {
fits = box.rotateLargestFootprint3D(freeSpace);
} else {
fits = box.fitRotate2D(freeSpace);
}
if(fits && box.getWeight() <= holder.getFreeWeight()) {
if(currentIndex == -1) {
currentIndex = i;
fullHeight = box.getHeight() == freeSpace.getHeight();
} else {
if(fullHeight) {
if(box.getHeight() == freeSpace.getHeight()) {
if(containerProducts.get(currentIndex).getFootprint() < box.getFootprint()) {
currentIndex = i;
}
}
} else {
if(box.getHeight() == freeSpace.getHeight()) {
fullHeight = true;
currentIndex = i;
} else if(footprintFirst) {
if(containerProducts.get(currentIndex).getFootprint() < box.getFootprint()) {
currentIndex = i;
} else if(containerProducts.get(currentIndex).getFootprint() == box.getFootprint() && containerProducts.get(currentIndex).getHeight() < box.getHeight()) {
currentIndex = i;
}
} else {
if(containerProducts.get(currentIndex).getHeight() < box.getHeight()) {
currentIndex = i;
} else if(containerProducts.get(currentIndex).getHeight() == box.getHeight() && containerProducts.get(currentIndex).getFootprint() < box.getFootprint()) {
currentIndex = i;
}
}
}
}
}
}
return currentIndex;
}
/**
* Remove from list, more explicit implementation than {@linkplain List#remove} with no equals.
* @param containerProducts list of products
* @param currentBox item to remove
*/
private void removeIdentical(List containerProducts, Box currentBox) {
for(int i = 0; i < containerProducts.size(); i++) {
if(containerProducts.get(i) == currentBox) {
containerProducts.remove(i);
return;
}
}
throw new IllegalArgumentException();
}
/**
* Fit in two dimensions
*
* @param containerProducts products to fit
* @param holder target container
* @param usedSpace space to subtract
* @param freeSpace available space
* @param interrupt interrupt
* @return false if interrupted
*/
protected boolean fit2D(List containerProducts, Container holder, Box usedSpace, Space freeSpace, BooleanSupplier interrupt) {
if(rotate3D) {
// minimize footprint
usedSpace.fitRotate3DSmallestFootprint(freeSpace);
}
// add used space box now, but possibly rotate later - this depends on the actual remaining free space selected further down
// there is up to possible 4 free spaces, 2 in which the used space box is rotated
holder.add(new Placement(freeSpace, usedSpace));
if(containerProducts.isEmpty()) {
// no additional boxes
// just make sure the used space fits in the free space
usedSpace.fitRotate2D(freeSpace);
return true;
}
if(interrupt.getAsBoolean()) {
return false;
}
Space[] spaces = getFreespaces(freeSpace, usedSpace);
Placement nextPlacement = getBestBoxAndSpace(containerProducts, spaces, holder.getFreeWeight());
if(nextPlacement == null) {
// no additional boxes along the level floor (x,y)
// just make sure the used space fits in the free space
usedSpace.fitRotate2D(freeSpace);
} else {
// check whether the selected free space requires the used space box to be rotated
if(nextPlacement.getSpace() == spaces[2] || nextPlacement.getSpace() == spaces[3]) {
// the desired space implies that we rotate the used space box
usedSpace.rotate2D();
}
// holder.validateCurrentLevel(); // uncomment for debugging
removeIdentical(containerProducts, nextPlacement.getBox());
// attempt to fit in the remaining (usually smaller) space first
// stack in the 'sibling' space - the space left over between the used box and the selected free space
Space remainder = nextPlacement.getSpace().getRemainder();
if(remainder.nonEmpty()) {
Box box = getBestBoxForSpace(containerProducts, remainder, holder.getFreeWeight());
if(box != null) {
removeIdentical(containerProducts, box);
if(!fit2D(containerProducts, holder, box, remainder, interrupt)) {
return false;
}
}
}
// double check that there is still free weight
// TODO possibly rewind if the value of the boxes in the remainder is lower than the one in next placement
if(holder.getFreeWeight() >= nextPlacement.getBox().getWeight()) {
if(!fit2D(containerProducts, holder, nextPlacement.getBox(), nextPlacement.getSpace(), interrupt)) {
return false;
}
}
}
// also use the space above the placed box, if any.
if(freeSpace.getHeight() > usedSpace.getHeight()) {
// so there is some free room; between the used space and the level height
// the level by level approach is somewhat crude, but at least some of the inefficiency
// can be avoided this way
Space above;
if(nextPlacement == null) {
// full width / depth
above = new Space(
freeSpace.getWidth(),
freeSpace.getDepth(),
freeSpace.getHeight() - usedSpace.getHeight(),
freeSpace.getX(),
freeSpace.getY(),
freeSpace.getZ() + usedSpace.getHeight()
);
} else {
// just directly above the used space
// TODO possible include the sibling space if no box was fitted there
above = new Space(
usedSpace.getWidth(),
usedSpace.getDepth(),
freeSpace.getHeight() - usedSpace.getHeight(),
freeSpace.getX(),
freeSpace.getY(),
freeSpace.getZ() + usedSpace.getHeight()
);
}
int currentIndex = getBestBox(holder, above, containerProducts);
if(currentIndex != -1) {
// should be within weight already
Box currentBox = containerProducts.get(currentIndex);
containerProducts.remove(currentIndex);
if(!fit2D(containerProducts, holder, currentBox, above, interrupt)) {
return false;
}
}
}
return true;
}
protected Space[] getFreespaces(Space freespace, Box used) {
// Two free spaces, on each rotation of the used space.
// Height is always the same, used box is assumed within free space height.
// First:
// ........................ ........................ .............
// . . . . . .
// . . . . . .
// . A . . A . . .
// . . . . . .
// . B . . . . B .
// ............ . ........................ . .
// . . . . .
// . . . . .
// ........................ .............
//
// Second:
//
// ........................ ........................ ..................
// . . . . . .
// . C . . C . . .
// . . . . . .
// ....... . ........................ . .
// . . D . . D .
// . . . . .
// . . . . .
// . . . . .
// ........................ ..................
//
// So there is always a 'big' and a 'small' leftover area (the small is not shown).
Space[] freeSpaces = new Space[4];
if(freespace.getWidth() >= used.getWidth() && freespace.getDepth() >= used.getDepth()) {
// if B is empty, then it is sufficient to work with A and the other way around
// B
if(freespace.getWidth() > used.getWidth()) {
Space right = new Space(
freespace.getWidth() - used.getWidth(), freespace.getDepth(), freespace.getHeight(),
freespace.getX() + used.getWidth(), freespace.getY(), freespace.getZ()
);
Space rightRemainder = new Space(
used.getWidth(), freespace.getDepth() - used.getDepth(), freespace.getHeight(),
freespace.getX(), freespace.getY() + used.getDepth(), freespace.getZ()
);
right.setRemainder(rightRemainder);
rightRemainder.setRemainder(right);
freeSpaces[0] = right;
}
// A
if(freespace.getDepth() > used.getDepth()) {
Space top = new Space(
freespace.getWidth(), freespace.getDepth() - used.getDepth(), freespace.getHeight(),
freespace.getX(), freespace.getY() + used.depth, freespace.getZ()
);
Space topRemainder = new Space(
freespace.getWidth() - used.getWidth(), used.getDepth(), freespace.getHeight(),
freespace.getX() + used.getWidth(), freespace.getY(), freespace.getZ()
);
top.setRemainder(topRemainder);
topRemainder.setRemainder(top);
freeSpaces[1] = top;
}
}
if(freespace.getWidth() >= used.getDepth() && freespace.getDepth() >= used.getWidth()) {
// if D is empty, then it is sufficient to work with C and the other way around
// D
if(freespace.getWidth() > used.getDepth()) {
Space right = new Space(
freespace.getWidth() - used.getDepth(), freespace.getDepth(), freespace.getHeight(),
freespace.getX() + used.getDepth(), freespace.getY(), freespace.getZ()
);
Space rightRemainder = new Space(
used.getDepth(), freespace.getDepth() - used.getWidth(), freespace.getHeight(),
freespace.getX(), freespace.getY() + used.getWidth(), freespace.getZ()
);
right.setRemainder(rightRemainder);
rightRemainder.setRemainder(right);
freeSpaces[2] = right;
}
// C
if(freespace.getDepth() > used.getWidth()) {
Space top = new Space(
freespace.getWidth(), freespace.getDepth() - used.getWidth(), freespace.getHeight(),
freespace.getX(), freespace.getY() + used.getWidth(), freespace.getZ()
);
Space topRemainder = new Space(
freespace.getWidth() - used.getDepth(), used.getWidth(), freespace.getHeight(),
freespace.getX() + used.getDepth(), freespace.getY(), freespace.getZ()
);
top.setRemainder(topRemainder);
topRemainder.setRemainder(top);
freeSpaces[3] = top;
}
}
return freeSpaces;
}
protected Box getBestBoxForSpace(List containerProducts, Space space, int freeWeight) {
Box bestBox = null;
for(Box box : containerProducts) {
if(box.getWeight() > freeWeight) {
continue;
}
if(rotate3D) {
if(box.canFitInside3D(space)) {
if(bestBox == null || isBetter3D(bestBox, box, space) < 0) {
bestBox = box;
}
}
} else {
if(box.canFitInside2D(space)) {
if(bestBox == null || isBetter2D(bestBox, box) < 0) {
bestBox = box;
}
}
}
}
return bestBox;
}
/**
* Is box b better than a?
*
* @param a box
* @param b box
* @return -1 if b is better, 0 if equal, 1 if b is better
*/
protected int isBetter2D(Box a, Box b) {
int compare = Long.compare(a.getVolume(), b.getVolume());
if(compare != 0) {
return compare;
}
return Long.compare(b.getFootprint(), a.getFootprint()); // i.e. smaller i better
}
/**
* Is box b strictly better than a?
*
* @param a box
* @param b box
* @param space free space
* @return -1 if b is better, 0 if equal, 1 if b is better
*/
protected int isBetter3D(Box a, Box b, Space space) {
int compare = Long.compare(a.getVolume(), b.getVolume());
if(compare != 0) {
return compare;
}
// determine lowest fit
a.fitRotate3DSmallestFootprint(space);
b.fitRotate3DSmallestFootprint(space);
return Long.compare(b.getFootprint(), a.getFootprint()); // i.e. smaller i better
}
protected Placement getBestBoxAndSpace(List containerProducts, Space[] spaces, int freeWeight) {
// this method could have many implementation
// it focuses on getting the biggest box possible fitted.
//
// an alternative implementation would be one that
// measure the amount of wasted space and/or maximizes largest leftover area
Box bestBox = null;
Space bestSpace = null;
for(Space space : spaces) {
if(space == null) {
continue;
}
Box box = getBestBoxForSpace(containerProducts, space, freeWeight);
if(box == null) {
continue;
}
boolean best;
if(bestBox == null) {
best = true;
} else {
int compare;
if(rotate3D) {
compare = isBetter3D(bestBox, box, space);
} else {
compare = isBetter2D(bestBox, box);
}
if(compare < 0) {
best = true;
} else if(compare == 0) {
// if all is equal, prefer smallest space as a crude indicator on
// how 'difficult' matching the box was.
best = space.getVolume() < bestSpace.getVolume();
} else {
best = false;
}
}
if(best) {
bestBox = box;
bestSpace = space;
}
}
if(bestBox != null) {
if(rotate3D) {
bestBox.fitRotate3DSmallestFootprint(bestSpace);
}
return new Placement(bestSpace, bestBox);
}
return null;
}
@Override
protected Adapter adapter() {
return new Adapter() {
private List boxes;
private LAFFResult previous;
private List containers;
@Override
public PackResult attempt(int index, BooleanSupplier interrupt) {
LAFFResult result = LargestAreaFitFirstPackager.this.pack(new ArrayList<>(boxes), containers.get(index), interrupt);
return previous = result;
}
@Override
public void initialize(List boxItems, List container) {
this.containers = container;
List boxClones = new ArrayList<>(boxItems.size() * 2);
for(BoxItem item : boxItems) {
Box box = item.getBox();
boxClones.add(box);
for(int i = 1; i < item.getCount(); i++) {
boxClones.add(box.clone());
}
}
this.boxes = boxClones;
}
@Override
public Container accepted(PackResult result) {
LAFFResult laffResult = (LAFFResult)result;
this.boxes = laffResult.getRemainingBoxes();
if(previous == result) {
return laffResult.getContainer();
}
// calculate again
Container container = laffResult.getContainer();
List boxes = new ArrayList<>(this.boxes.size());
for(Level level : container.getLevels()) {
for(Placement placement : level) {
boxes.add(placement.getBox());
}
}
container.clear();
LAFFResult pack = LargestAreaFitFirstPackager.this.pack(boxes, container, Long.MAX_VALUE);
return pack.getContainer();
}
@Override
public boolean hasMore(PackResult result) {
LAFFResult laffResult = (LAFFResult)result;
return !laffResult.getRemainingBoxes().isEmpty();
}
};
}
}