All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.skjolberg.packing.Packager Maven / Gradle / Ivy

There is a newer version: 1.2.5
Show newest version
package com.github.skjolberg.packing;

import com.github.skjolberg.packing.impl.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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.
 */

public abstract class Packager {
	private final Container[] containers;

	final boolean rotate3D; // if false, then 2d
	private final boolean binarySearch;

	/**
	 * Constructor
	 *
	 * @param containers list of containers
	 */

	public Packager(List containers) {
		this(containers, true, true);
	}

	/**
	 * Constructor
	 *
	 * @param containers   list of containers
	 * @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
	 *                     match, it searches the preceding boxes as well, until the deadline is passed.
	 */

	public Packager(List containers, boolean rotate3D, boolean binarySearch) {
		this.containers = containers.toArray(new Container[0]);
		this.rotate3D = rotate3D;
		this.binarySearch = binarySearch;

		long maxVolume = Long.MIN_VALUE;
		long maxWeight = Long.MIN_VALUE;

		for (Container container : containers) {
			// volume
			long boxVolume = container.getVolume();
			if (boxVolume > maxVolume) {
				maxVolume = boxVolume;
			}

			// weight
			long boxWeight = container.getWeight();
			if (boxWeight > maxWeight) {
				maxWeight = boxWeight;
			}
		}
	}

	/**
	 * Return a container which holds all the boxes in the argument.
	 *
	 * @param boxes list of boxes to fit in a container
	 * @return null if no match
	 */
	public Container pack(List boxes) {
		return pack(boxes, Long.MAX_VALUE);
	}

	/**
	 * Return a container which holds all the boxes in the argument
	 *
	 * @param boxes    list of boxes to fit in a container
	 * @param deadline the system time in millis at which the search should be aborted
	 * @return index of container if match, -1 if not
	 */

	public Container pack(List boxes, long deadline) {
		return pack(boxes, filterByVolumeAndWeight(toBoxes(boxes, false), Arrays.asList(containers), 1), deadLinePredicate(deadline));
	}

	public Container pack(List boxes, BooleanSupplier interrupt) {
		return pack(boxes, filterByVolumeAndWeight(toBoxes(boxes, false), Arrays.asList(containers), 1), interrupt);
	}

	/**
	 * Return a container which holds all the boxes in the argument
	 *
	 * @param boxes     list of boxes to fit in a container
	 * @param deadline  the system time in millis at which the search should be aborted
	 * @param interrupt When true, the computation is interrupted as soon as possible.
	 * @return index of container if match, -1 if not
	 */
	public Container pack(List boxes, long deadline, AtomicBoolean interrupt) {
		return pack(boxes, filterByVolumeAndWeight(toBoxes(boxes, false), Arrays.asList(containers), 1), deadline, interrupt);
	}

	/**
	 * Return a container which holds all the boxes in the argument
	 *
	 * @param boxes      list of boxes to fit in a container
	 * @param containers list of containers
	 * @param deadline   the system time in milliseconds at which the search should be aborted
	 * @return index of container if match, -1 if not
	 */
	public Container pack(List boxes, List containers, long deadline) {
		return pack(boxes, containers, deadLinePredicate(deadline));
	}

	/**
	 * Return a container which holds all the boxes in the argument
	 *
	 * @param boxes      list of boxes to fit in a container
	 * @param containers list of containers
	 * @param deadline   the system time in milliseconds at which the search should be aborted
	 * @param interrupt  When true, the computation is interrupted as soon as possible.
	 * @return index of container if match, -1 if not
	 */
	public Container pack(List boxes, List containers, long deadline, AtomicBoolean interrupt) {
		return pack(boxes, containers, () -> deadlineReached(deadline) || interrupt.get());
	}

	public Container pack(List boxes, List containers, BooleanSupplier interrupt) {
		if (containers.isEmpty()) {
			return null;
		}

		Adapter pack = adapter();
		pack.initialize(boxes, containers);

		if (!binarySearch || containers.size() <= 2) {
			for (int i = 0; i < containers.size(); i++) {

				if (interrupt.getAsBoolean()) {
					break;
				}

				PackResult result = pack.attempt(i, interrupt);
				if (result == null) {
					return null; // timeout
				}

				if (!pack.hasMore(result)) {
					return pack.accepted(result);
				}
			}
		} else {
			// perform a binary search among the available containers
			// the list is ranked from most desirable to least.
			PackResult[] results = new PackResult[containers.size()];
			boolean[] checked = new boolean[results.length];

			ArrayList containerIndexes = new ArrayList<>(containers.size());
			for (int i = 0; i < containers.size(); i++) {
				containerIndexes.add(i);
			}

			BinarySearchIterator iterator = new BinarySearchIterator();

			search:
			do {
				iterator.reset(containerIndexes.size() - 1, 0);

				do {
					int next = iterator.next();
					int mid = containerIndexes.get(next);

					PackResult result = pack.attempt(mid, interrupt);
					if (result == null) {
						return null; // timeout
					}
					checked[mid] = true;
					if (!pack.hasMore(result)) {
						results[mid] = result;

						iterator.lower();
					} else {
						iterator.higher();
					}
					if (interrupt.getAsBoolean()) {
						break search;
					}
				} while (iterator.hasNext());

				// halt when have a result, and checked all containers at the lower indexes
				for (int i = 0; i < containerIndexes.size(); i++) {
					Integer integer = containerIndexes.get(i);
					if (results[integer] != null) {
						// remove end items; we already have a better match
						while (containerIndexes.size() > i) {
							containerIndexes.remove(containerIndexes.size() - 1);
						}
						break;
					}

					// remove item
					if (checked[integer]) {
						containerIndexes.remove(i);
						i--;
					}
				}
			} while (!containerIndexes.isEmpty());

			for (final PackResult result : results) {
				if (result != null) {
					return pack.accepted(result);
				}
			}
		}
		return null;
	}

	/**
	 * Return a list of containers which holds all the boxes in the argument
	 *
	 * @param boxes    list of boxes to fit in a container
	 * @param limit    maximum number of containers
	 * @param deadline the system time in milliseconds at which the search should be aborted
	 * @return index of container if match, -1 if not
	 */
	public List packList(List boxes, int limit, long deadline) {
		return packList(boxes, limit, deadLinePredicate(deadline));
	}

	static BooleanSupplier deadLinePredicate(final long deadline) {
		return () -> deadlineReached(deadline);
	}

	static boolean deadlineReached(final long deadline) {
		return System.currentTimeMillis() > deadline;
	}

	/**
	 * Return a list of containers which holds all the boxes in the argument
	 *
	 * @param boxes     list of boxes to fit in a container
	 * @param limit     maximum number of containers
	 * @param deadline  the system time in milliseconds at which the search should be aborted
	 * @param interrupt When true, the computation is interrupted as soon as possible.
	 * @return index of container if match, -1 if not
	 */
	public List packList(List boxes, int limit, long deadline, AtomicBoolean interrupt) {
		return packList(boxes, limit, () -> deadlineReached(deadline) || interrupt.get());
	}

	/**
	 * Return a list of containers which holds all the boxes in the argument
	 *
	 * @param boxes     list of boxes to fit in a container
	 * @param limit     maximum number of containers
	 * @param interrupt When true, the computation is interrupted as soon as possible.
	 * @return index of container if match, -1 if not
	 */
	public List packList(List boxes, int limit, BooleanSupplier interrupt) {
		List containers = filterByVolumeAndWeight(toBoxes(boxes, true), Arrays.asList(this.containers), limit);
		if (containers.isEmpty()) {
			return null;
		}

		Adapter pack = adapter();
		pack.initialize(boxes, containers);

		List containerPackResults = new ArrayList<>();

		// binary search: not as simple as in the single-container use-case; discarding containers would need some kind
		// of criteria which could be trivially calculated, perhaps on volume.
		do {
			PackResult best = null;
			for (int i = 0; i < containers.size(); i++) {

				if (interrupt.getAsBoolean()) {
					return null;
				}

				PackResult result = pack.attempt(i, interrupt);
				if (result == null) {
					return null; // timeout
				}

				if (!result.isEmpty()) {
					if (best == null || result.packsMoreBoxesThan(best)) {
						best = result;

						if (!pack.hasMore(best)) { // will not match any better than this
							break;
						}
					}
				}
			}

			if (best == null) {
				// negative result
				return null;
			}

			boolean end = !pack.hasMore(best);

			containerPackResults.add(pack.accepted(best));

			if (end) {
				// positive result
				return containerPackResults;
			}

		} while (containerPackResults.size() < limit);

		return null;
	}

	/**
	 * Return a list of containers which can potentially hold the boxes.
	 *
	 * @param boxes      list of boxes
	 * @param containers list of containers
	 * @param count      maximum number of possible containers
	 * @return list of containers
	 */
	private List filterByVolumeAndWeight(List boxes, List containers, int count) {
		long volume = 0;
		long minVolume = Long.MAX_VALUE;

		long weight = 0;
		long minWeight = Long.MAX_VALUE;

		for (Box box : boxes) {
			// volume
			long boxVolume = box.getVolume();
			volume += boxVolume;

			if (boxVolume < minVolume) {
				minVolume = boxVolume;
			}

			// weight
			long boxWeight = box.getWeight();
			weight += boxWeight;

			if (boxWeight < minWeight) {
				minWeight = boxWeight;
			}
		}

		long maxVolume = Long.MIN_VALUE;
		long maxWeight = Long.MIN_VALUE;

		for (Container container : containers) {
			// volume
			long boxVolume = container.getVolume();
			if (boxVolume > maxVolume) {
				maxVolume = boxVolume;
			}

			// weight
			long boxWeight = container.getWeight();
			if (boxWeight > maxWeight) {
				maxWeight = boxWeight;
			}
		}

		if (maxVolume * count < volume || maxWeight * count < weight) {
			// no containers will work at current count
			return Collections.emptyList();
		}

		List list = new ArrayList<>(containers.size());
		for (Container container : containers) {
			if (container.getVolume() < minVolume || container.getWeight() < minWeight) {
				// this box cannot even fit a single box
				continue;
			}

			if (container.getVolume() + maxVolume * (count - 1) < volume || container.getWeight() + maxWeight * (count - 1) < weight) {
				// this box cannot be used even together with all biggest boxes
				continue;
			}

			if (count == 1) {
				if (!canHoldAll(container, boxes)) {
					continue;
				}
			} else {
				if (!canHoldAtLeastOne(container, boxes)) {
					continue;
				}
			}

			list.add(container);
		}

		return list;
	}


	private static List toBoxes(List boxItems, boolean clone) {
		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(clone ? box : box.clone());
			}
		}
		return boxClones;

	}


	protected abstract Adapter adapter();

	private boolean canHoldAll(Container containerBox, List boxes) {
		for (Box box : boxes) {
			if (containerBox.getWeight() < box.getWeight()) {
				continue;
			}
			if (rotate3D) {
				if (!containerBox.canHold3D(box)) {
					return false;
				}
			} else {
				if (!containerBox.canHold2D(box)) {
					return false;
				}
			}
		}
		return true;
	}


	private boolean canHoldAtLeastOne(Container containerBox, List boxes) {
		for (Box box : boxes) {
			if (containerBox.getWeight() < box.getWeight()) {
				continue;
			}
			if (rotate3D) {
				if (containerBox.canHold3D(box)) {
					return true;
				}
			} else {
				if (containerBox.canHold2D(box)) {
					return true;
				}
			}
		}
		return false;
	}


	static List getPlacements(int size) {
		// each box will at most have a single placement with a space (and its remainder).
		List placements = new ArrayList<>(size);

		for (int i = 0; i < size; i++) {
			Space a = new Space();
			Space b = new Space();
			a.setRemainder(b);
			b.setRemainder(a);

			placements.add(new Placement(a));
		}
		return placements;
	}


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy