/*******************************************************************************
* Copyright (C) 2014 Stefan Schroeder
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package jsprit.core.problem;
import jsprit.core.problem.cost.VehicleRoutingActivityCosts;
import jsprit.core.problem.cost.VehicleRoutingTransportCosts;
import jsprit.core.problem.driver.Driver;
import jsprit.core.problem.job.Job;
import jsprit.core.problem.job.Service;
import jsprit.core.problem.job.Shipment;
import jsprit.core.problem.solution.route.VehicleRoute;
import jsprit.core.problem.solution.route.activity.DefaultShipmentActivityFactory;
import jsprit.core.problem.solution.route.activity.DefaultTourActivityFactory;
import jsprit.core.problem.solution.route.activity.TourActivity;
import jsprit.core.problem.vehicle.*;
import jsprit.core.util.Coordinate;
import jsprit.core.util.CrowFlyCosts;
import jsprit.core.util.Locations;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.*;
/**
* Contains and defines the vehicle routing problem.
*
* <p>A routing problem is defined as jobs, vehicles, costs and constraints.
*
* <p> To construct the problem, use VehicleRoutingProblem.Builder. Get an instance of this by using the static method VehicleRoutingProblem.Builder.newInstance().
*
* <p>By default, fleetSize is INFINITE, transport-costs are calculated as euclidean-distance (CrowFlyCosts),
* and activity-costs are set to zero.
*
*
*
* @author stefan schroeder
*
*/
public class VehicleRoutingProblem {
/**
* Builder to build the routing-problem.
*
* @author stefan schroeder
*
*/
public static class Builder {
/**
* Returns a new instance of this builder.
*
* @return builder
*/
public static Builder newInstance(){ return new Builder(); }
private VehicleRoutingTransportCosts transportCosts;
private VehicleRoutingActivityCosts activityCosts = new VehicleRoutingActivityCosts() {
@Override
public double getActivityCost(TourActivity tourAct, double arrivalTime, Driver driver, Vehicle vehicle) {
return 0;
}
@Override
public String toString() {
return "[name=defaultActivityCosts]";
}
};
private Map<String,Job> jobs = new HashMap<String, Job>();
private Map<String,Job> tentativeJobs = new HashMap<String,Job>();
private Set<String> jobsInInitialRoutes = new HashSet<String>();
private Map<String, Coordinate> tentative_coordinates = new HashMap<String, Coordinate>();
private FleetSize fleetSize = FleetSize.INFINITE;
private Collection<VehicleType> vehicleTypes = new ArrayList<VehicleType>();
private Collection<VehicleRoute> initialRoutes = new ArrayList<VehicleRoute>();
private Set<Vehicle> uniqueVehicles = new HashSet<Vehicle>();
private JobActivityFactory jobActivityFactory = new JobActivityFactory() {
@Override
public List<AbstractActivity> createActivities(Job job) {
List<AbstractActivity> acts = new ArrayList<AbstractActivity>();
if(job instanceof Service){
acts.add(serviceActivityFactory.createActivity((Service) job));
}
else if(job instanceof Shipment){
acts.add(shipmentActivityFactory.createPickup((Shipment) job));
acts.add(shipmentActivityFactory.createDelivery((Shipment) job));
}
return acts;
}
};
private boolean addPenaltyVehicles = false;
private double penaltyFactor = 1.0;
private Double penaltyFixedCosts = null;
private int jobIndexCounter = 1;
private int vehicleIndexCounter = 1;
private int activityIndexCounter = 1;
private int vehicleTypeIdIndexCounter = 1;
private Map<VehicleTypeKey,Integer> typeKeyIndices = new HashMap<VehicleTypeKey, Integer>();
private Map<Job,List<AbstractActivity>> activityMap = new HashMap<Job, List<AbstractActivity>>();
private final DefaultShipmentActivityFactory shipmentActivityFactory = new DefaultShipmentActivityFactory();
private final DefaultTourActivityFactory serviceActivityFactory = new DefaultTourActivityFactory();
/**
* Create a location (i.e. coordinate) and returns the key of the location which is Coordinate.toString().
*
* @param x x-coordinate of location
* @param y y-coordinate of location
* @return locationId
* @see Coordinate
*/
@SuppressWarnings("UnusedDeclaration")
@Deprecated
public String createLocation(double x, double y){
Coordinate coordinate = new Coordinate(x, y);
String id = coordinate.toString();
if(!tentative_coordinates.containsKey(id)){
tentative_coordinates.put(id, coordinate);
}
return id;
}
private void incJobIndexCounter(){
jobIndexCounter++;
}
private void incActivityIndexCounter(){
activityIndexCounter++;
}
private void incVehicleTypeIdIndexCounter() { vehicleTypeIdIndexCounter++; }
/**
* Returns the unmodifiable map of collected locations (mapped by their location-id).
*
* @return map with locations
*/
public Map<String,Coordinate> getLocationMap(){
return Collections.unmodifiableMap(tentative_coordinates);
}
/**
* Returns the locations collected SO FAR by this builder.
*
* <p>Locations are cached when adding a shipment, service, depot, vehicle.
*
* @return locations
*
**/
public Locations getLocations(){
return new Locations() {
@Override
public Coordinate getCoord(String id) {
return tentative_coordinates.get(id);
}
};
}
/**
* Sets routing costs.
*
* @param costs the routingCosts
* @return builder
* @see VehicleRoutingTransportCosts
*/
public Builder setRoutingCost(VehicleRoutingTransportCosts costs){
this.transportCosts = costs;
return this;
}
/**
* Sets the type of fleetSize.
*
* <p>FleetSize is either FleetSize.INFINITE or FleetSize.FINITE. By default it is FleetSize.INFINITE.
*
* @param fleetSize the fleet size used in this problem. it can either be FleetSize.INFINITE or FleetSize.FINITE
* @return this builder
*/
public Builder setFleetSize(FleetSize fleetSize){
this.fleetSize = fleetSize;
return this;
}
/**
* Adds a job which is either a service or a shipment.
*
* <p>Note that job.getId() must be unique, i.e. no job (either it is a shipment or a service) is allowed to have an already allocated id.
*
* @param job job to be added
* @return this builder
* @throws IllegalStateException if job is neither a shipment nor a service, or jobId has already been added.
* @deprecated use addJob(AbstractJob job) instead
*/
@Deprecated
public Builder addJob(Job job) {
if(!(job instanceof AbstractJob)) throw new IllegalArgumentException("job must be of type AbstractJob");
return addJob((AbstractJob)job);
}
/**
* Adds a job which is either a service or a shipment.
*
* <p>Note that job.getId() must be unique, i.e. no job (either it is a shipment or a service) is allowed to have an already allocated id.
*
* @param job job to be added
* @return this builder
* @throws IllegalStateException if job is neither a shipment nor a service, or jobId has already been added.
*/
public Builder addJob(AbstractJob job) {
if(tentativeJobs.containsKey(job.getId())) throw new IllegalStateException("jobList already contains a job with id " + job.getId() + ". make sure you use unique ids for your jobs (i.e. service and shipments)");
if(!(job instanceof Service || job instanceof Shipment)) throw new IllegalStateException("job must be either a service or a shipment");
job.setIndex(jobIndexCounter);
incJobIndexCounter();
tentativeJobs.put(job.getId(), job);
addLocationToTentativeLocations(job);
return this;
}
private void addLocationToTentativeLocations(Job job) {
if(job instanceof Service) {
tentative_coordinates.put(((Service)job).getLocationId(), ((Service)job).getCoord());
}
else if(job instanceof Shipment){
Shipment shipment = (Shipment)job;
tentative_coordinates.put(shipment.getPickupLocationId(), shipment.getPickupCoord());
tentative_coordinates.put(shipment.getDeliveryLocationId(), shipment.getDeliveryCoord());
}
}
private void addJobToFinalJobMapAndCreateActivities(Job job){
if(job instanceof Service) {
Service service = (Service) job;
addService(service);
}
else if(job instanceof Shipment){
Shipment shipment = (Shipment)job;
addShipment(shipment);
}
List<AbstractActivity> jobActs = jobActivityFactory.createActivities(job);
for(AbstractActivity act : jobActs){
act.setIndex(activityIndexCounter);
incActivityIndexCounter();
}
activityMap.put(job, jobActs);
}
/**
* Adds an initial vehicle route.
*
* @param route initial route
* @return the builder
*/
public Builder addInitialVehicleRoute(VehicleRoute route){
addVehicle((AbstractVehicle)route.getVehicle());
for(TourActivity act : route.getActivities()){
AbstractActivity abstractAct = (AbstractActivity) act;
abstractAct.setIndex(activityIndexCounter);
incActivityIndexCounter();
if(act instanceof TourActivity.JobActivity) {
Job job = ((TourActivity.JobActivity) act).getJob();
jobsInInitialRoutes.add(job.getId());
registerLocation(job);
registerJobAndActivity(abstractAct, job);
}
}
initialRoutes.add(route);
return this;
}
private void registerLocation(Job job) {
if (job instanceof Service) tentative_coordinates.put(((Service) job).getLocationId(), ((Service) job).getCoord());
if (job instanceof Shipment) {
Shipment shipment = (Shipment) job;
tentative_coordinates.put(shipment.getPickupLocationId(), shipment.getPickupCoord());
tentative_coordinates.put(shipment.getDeliveryLocationId(), shipment.getDeliveryCoord());
}
}
private void registerJobAndActivity(AbstractActivity abstractAct, Job job) {
if(activityMap.containsKey(job)) activityMap.get(job).add(abstractAct);
else{
List<AbstractActivity> actList = new ArrayList<AbstractActivity>();
actList.add(abstractAct);
activityMap.put(job,actList);
}
}
/**
* Adds a collection of initial vehicle routes.
*
* @param routes initial routes
* @return the builder
*/
public Builder addInitialVehicleRoutes(Collection<VehicleRoute> routes){
for(VehicleRoute r : routes){
addInitialVehicleRoute(r);
}
return this;
}
private void addShipment(Shipment job) {
if(jobs.containsKey(job.getId())){ logger.warn("job " + job + " already in job list. overrides existing job."); }
tentative_coordinates.put(job.getPickupLocationId(), job.getPickupCoord());
tentative_coordinates.put(job.getDeliveryLocationId(), job.getDeliveryCoord());
jobs.put(job.getId(),job);
}
/**
* Adds a vehicle.
*
*
* @param vehicle vehicle to be added
* @return this builder
* @deprecated use addVehicle(AbstractVehicle vehicle) instead
*/
@Deprecated
public Builder addVehicle(Vehicle vehicle) {
if(!(vehicle instanceof AbstractVehicle)) throw new IllegalStateException("vehicle must be an AbstractVehicle");
return addVehicle((AbstractVehicle)vehicle);
}
/**
* Adds a vehicle.
*
*
* @param vehicle vehicle to be added
* @return this builder
*/
public Builder addVehicle(AbstractVehicle vehicle) {
if(!uniqueVehicles.contains(vehicle)){
vehicle.setIndex(vehicleIndexCounter);
incVehicleIndexCounter();
}
if(typeKeyIndices.containsKey(vehicle.getVehicleTypeIdentifier())){
vehicle.getVehicleTypeIdentifier().setIndex(typeKeyIndices.get(vehicle.getVehicleTypeIdentifier()));
}
else {
vehicle.getVehicleTypeIdentifier().setIndex(vehicleTypeIdIndexCounter);
typeKeyIndices.put(vehicle.getVehicleTypeIdentifier(),vehicleTypeIdIndexCounter);
incVehicleTypeIdIndexCounter();
}
uniqueVehicles.add(vehicle);
if(!vehicleTypes.contains(vehicle.getType())){
vehicleTypes.add(vehicle.getType());
}
String startLocationId = vehicle.getStartLocationId();
tentative_coordinates.put(startLocationId, vehicle.getStartLocationCoordinate());
if(!vehicle.getEndLocationId().equals(startLocationId)){
tentative_coordinates.put(vehicle.getEndLocationId(), vehicle.getEndLocationCoordinate());
}
return this;
}
private void incVehicleIndexCounter() {
vehicleIndexCounter++;
}
/**
* Sets the activity-costs.
*
* <p>By default it is set to zero.
*
* @param activityCosts activity costs of the problem
* @return this builder
* @see VehicleRoutingActivityCosts
*/
public Builder setActivityCosts(VehicleRoutingActivityCosts activityCosts){
this.activityCosts = activityCosts;
return this;
}
/**
* Builds the {@link VehicleRoutingProblem}.
*
* <p>If {@link VehicleRoutingTransportCosts} are not set, {@link CrowFlyCosts} is used.
*
* @return {@link VehicleRoutingProblem}
*/
public VehicleRoutingProblem build() {
logger.info("build problem ...");
if(transportCosts == null){
logger.warn("set routing costs crowFlyDistance.");
transportCosts = new CrowFlyCosts(getLocations());
}
if(addPenaltyVehicles){
if(fleetSize.equals(FleetSize.INFINITE)){
logger.warn("penaltyType and FleetSize.INFINITE does not make sense. thus no penalty-types are added.");
}
else{
addPenaltyVehicles();
}
}
for(Job job : tentativeJobs.values())
if (!jobsInInitialRoutes.contains(job.getId())) {
addJobToFinalJobMapAndCreateActivities(job);
}
return new VehicleRoutingProblem(this);
}
private void addPenaltyVehicles() {
Set<VehicleTypeKey> vehicleTypeKeys = new HashSet<VehicleTypeKey>();
List<Vehicle> uniqueVehicles = new ArrayList<Vehicle>();
for(Vehicle v : this.uniqueVehicles){
VehicleTypeKey key = new VehicleTypeKey(v.getType().getTypeId(),v.getStartLocationId(),v.getEndLocationId(),v.getEarliestDeparture(),v.getLatestArrival(), v.getSkills());
if(!vehicleTypeKeys.contains(key)){
uniqueVehicles.add(v);
vehicleTypeKeys.add(key);
}
}
for(Vehicle v : uniqueVehicles){
double fixed = v.getType().getVehicleCostParams().fix * penaltyFactor;
if(penaltyFixedCosts!=null){
fixed = penaltyFixedCosts;
}
VehicleTypeImpl t = VehicleTypeImpl.Builder.newInstance(v.getType().getTypeId())
.setCostPerDistance(penaltyFactor*v.getType().getVehicleCostParams().perDistanceUnit)
.setCostPerTime(penaltyFactor*v.getType().getVehicleCostParams().perTimeUnit)
.setFixedCost(fixed)
.setCapacityDimensions(v.getType().getCapacityDimensions())
.build();
PenaltyVehicleType penType = new PenaltyVehicleType(t,penaltyFactor);
String vehicleId = v.getId();
VehicleImpl penVehicle = VehicleImpl.Builder.newInstance(vehicleId).setEarliestStart(v.getEarliestDeparture())
.setLatestArrival(v.getLatestArrival()).setStartLocationCoordinate(v.getStartLocationCoordinate()).setStartLocationId(v.getStartLocationId())
.setEndLocationId(v.getEndLocationId()).setEndLocationCoordinate(v.getEndLocationCoordinate())
.addSkills(v.getSkills())
.setReturnToDepot(v.isReturnToDepot()).setType(penType).build();
addVehicle(penVehicle);
}
}
@SuppressWarnings("UnusedDeclaration")
public Builder addLocation(String locationId, Coordinate coordinate) {
tentative_coordinates.put(locationId, coordinate);
return this;
}
/**
* Adds a collection of jobs.
*
* @param jobs which is a collection of jobs that subclasses Job
* @return this builder
*/
@SuppressWarnings("deprecation")
public Builder addAllJobs(Collection<? extends Job> jobs) {
for(Job j : jobs){
addJob(j);
}
return this;
}
/**
* Adds a collection of vehicles.
*
* @param vehicles vehicles to be added
* @return this builder
*/
@SuppressWarnings("deprecation")
public Builder addAllVehicles(Collection<? extends Vehicle> vehicles) {
for(Vehicle v : vehicles){
addVehicle(v);
}
return this;
}
/**
* Gets an unmodifiable collection of already added vehicles.
*
* @return collection of vehicles
*/
public Collection<Vehicle> getAddedVehicles(){
return Collections.unmodifiableCollection(uniqueVehicles);
}
/**
* Gets an unmodifiable collection of already added vehicle-types.
*
* @return collection of vehicle-types
*/
public Collection<VehicleType> getAddedVehicleTypes(){
return Collections.unmodifiableCollection(vehicleTypes);
}
/**
* Adds penaltyVehicles, i.e. for every unique vehicle-location and type combination a penalty-vehicle is constructed having penaltyFactor times higher fixed and variable costs
* (see .addPenaltyVehicles(double penaltyFactor, double penaltyFixedCosts) if fixed costs = 0.0).
*
* <p>This only makes sense for FleetSize.FINITE. Thus, penaltyVehicles are only added if is FleetSize.FINITE.
* <p>The id of penaltyVehicles is constructed as follows vehicleId = "penaltyVehicle" + "_" + {locationId} + "_" + {typeId}.
* <p>By default: no penalty-vehicles are added
*
* @param penaltyFactor penaltyFactor of penaltyVehicle
* @return this builder
* @deprecated since 1.3.2-SNAPSHOT bad job list replaces penalty vehicles
*/
@Deprecated
public Builder addPenaltyVehicles(double penaltyFactor){
this.addPenaltyVehicles = true;
this.penaltyFactor = penaltyFactor;
return this;
}
/**
* Adds penaltyVehicles, i.e. for every unique vehicle-location and type combination a penalty-vehicle is constructed having penaltyFactor times higher fixed and variable costs.
* <p>This method takes penaltyFixedCosts as absolute value in contrary to the method without penaltyFixedCosts where fixedCosts is the product of penaltyFactor and typeFixedCosts.
* <p>This only makes sense for FleetSize.FINITE. Thus, penaltyVehicles are only added if is FleetSize.FINITE.
* <p>The id of penaltyVehicles is constructed as follows vehicleId = "penaltyVehicle" + "_" + {locationId} + "_" + {typeId}.
* <p>By default: no penalty-vehicles are added
*
* @param penaltyFactor the penaltyFactor of penaltyVehicle
* @param penaltyFixedCosts which is an absolute penaltyValue (in contrary to penaltyFactor)
* @return this builder
* @deprecated since 1.3.2-SNAPSHOT bad job list replaces penalty vehicles
*/
@Deprecated
public Builder addPenaltyVehicles(double penaltyFactor, double penaltyFixedCosts){
this.addPenaltyVehicles = true;
this.penaltyFactor = penaltyFactor;
this.penaltyFixedCosts = penaltyFixedCosts;
return this;
}
/**
* Returns an unmodifiable collection of already added jobs.
*
* @return collection of jobs
*/
public Collection<Job> getAddedJobs(){
return Collections.unmodifiableCollection(tentativeJobs.values());
}
private Builder addService(Service service){
tentative_coordinates.put(service.getLocationId(), service.getCoord());
if(jobs.containsKey(service.getId())){ logger.warn("service " + service + " already in job list. overrides existing job."); }
jobs.put(service.getId(),service);
return this;
}
}
/**
* Enum that characterizes the fleet-size.
*
* @author sschroeder
*
*/
public static enum FleetSize {
FINITE, INFINITE
}
/**
* logger logging for this class
*/
private final static Logger logger = LogManager.getLogger(VehicleRoutingProblem.class);
/**
* contains transportation costs, i.e. the costs traveling from location A to B
*/
private final VehicleRoutingTransportCosts transportCosts;
/**
* contains activity costs, i.e. the costs imposed by an activity
*/
private final VehicleRoutingActivityCosts activityCosts;
/**
* map of jobs, stored by jobId
*/
private final Map<String, Job> jobs;
/**
* Collection that contains available vehicles.
*/
private final Collection<Vehicle> vehicles;
/**
* Collection that contains all available types.
*/
private final Collection<VehicleType> vehicleTypes;
private final Collection<VehicleRoute> initialVehicleRoutes;
/**
* An enum that indicates type of fleetSize. By default, it is INFINTE
*/
private final FleetSize fleetSize;
private final Locations locations;
private Map<Job,List<AbstractActivity>> activityMap;
private int nuActivities;
private final JobActivityFactory jobActivityFactory = new JobActivityFactory() {
@Override
public List<AbstractActivity> createActivities(Job job) {
return copyAndGetActivities(job);
}
};
private VehicleRoutingProblem(Builder builder) {
this.jobs = builder.jobs;
this.fleetSize = builder.fleetSize;
this.vehicles=builder.uniqueVehicles;
this.vehicleTypes = builder.vehicleTypes;
this.initialVehicleRoutes = builder.initialRoutes;
this.transportCosts = builder.transportCosts;
this.activityCosts = builder.activityCosts;
this.locations = builder.getLocations();
this.activityMap = builder.activityMap;
this.nuActivities = builder.activityIndexCounter;
logger.info("initialise " + this);
}
@Override
public String toString() {
return "[fleetSize="+fleetSize+"][#jobs="+jobs.size()+"][#vehicles="+vehicles.size()+"][#vehicleTypes="+vehicleTypes.size()+"]["+
"transportCost="+transportCosts+"][activityCosts="+activityCosts+"]";
}
/**
* Returns type of fleetSize, either INFINITE or FINITE.
*
* <p>By default, it is INFINITE.
*
* @return either FleetSize.INFINITE or FleetSize.FINITE
*/
public FleetSize getFleetSize() {
return fleetSize;
}
/**
* Returns the unmodifiable job map.
*
* @return unmodifiable jobMap
*/
public Map<String, Job> getJobs() {
return Collections.unmodifiableMap(jobs);
}
/**
* Returns a copy of initial vehicle routes.
*
* @return copied collection of initial vehicle routes
*/
public Collection<VehicleRoute> getInitialVehicleRoutes(){
Collection<VehicleRoute> copiedInitialRoutes = new ArrayList<VehicleRoute>();
for(VehicleRoute route : initialVehicleRoutes){
copiedInitialRoutes.add(VehicleRoute.copyOf(route));
}
return copiedInitialRoutes;
}
/**
* Returns the entire, unmodifiable collection of types.
*
* @return unmodifiable collection of types
* @see VehicleTypeImpl
*/
public Collection<VehicleType> getTypes(){
return Collections.unmodifiableCollection(vehicleTypes);
}
/**
* Returns the entire, unmodifiable collection of vehicles.
*
* @return unmodifiable collection of vehicles
* @see Vehicle
*/
public Collection<Vehicle> getVehicles() {
return Collections.unmodifiableCollection(vehicles);
}
/**
* Returns routing costs.
*
* @return routingCosts
* @see VehicleRoutingTransportCosts
*/
public VehicleRoutingTransportCosts getTransportCosts() {
return transportCosts;
}
/**
* Returns activityCosts.
*/
public VehicleRoutingActivityCosts getActivityCosts(){
return activityCosts;
}
/**
* @return returns all location, i.e. from vehicles and jobs.
*/
public Locations getLocations(){
return locations;
}
/**
* @param job for which the corresponding activities needs to be returned
* @return associated activities
*/
public List<AbstractActivity> getActivities(Job job){
return Collections.unmodifiableList(activityMap.get(job));
}
/**
* @return total number of activities
*/
public int getNuActivities(){ return nuActivities; }
/**
* @return factory that creates the activities associated to a job
*/
public JobActivityFactory getJobActivityFactory(){
return jobActivityFactory;
}
/**
* @param job for which the corresponding activities needs to be returned
* @return a copy of the activities that are associated to the specified job
*/
public List<AbstractActivity> copyAndGetActivities(Job job){
List<AbstractActivity> acts = new ArrayList<AbstractActivity>();
if(activityMap.containsKey(job)) {
for (AbstractActivity act : activityMap.get(job)) acts.add((AbstractActivity) act.duplicate());
}
return acts;
}
}