" : "'" + description + "'") + " " + coordinates;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/Route.java
================================================
package org.optaweb.vehiclerouting.domain;
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Vehicle's itinerary (sequence of visits) and its depot. This entity cannot exist without the vehicle and the depot
* but it's allowed to have no visits when the vehicle hasn't been assigned any (it's idle).
*
* This entity describes part of a {@link RoutingPlan solution} of the vehicle routing problem
* (assignment of a subset of visits to one of the vehicles).
* It doesn't carry the data about physical tracks between adjacent visits.
* Geographical data is held by {@link RouteWithTrack}.
*/
public class Route {
private final Vehicle vehicle;
private final Location depot;
private final List visits;
/**
* Create a vehicle route.
*
* @param vehicle the vehicle assigned to this route (not {@code null})
* @param depot vehicle's depot (not {@code null})
* @param visits list of visits (not {@code null})
*/
public Route(Vehicle vehicle, Location depot, List visits) {
this.vehicle = Objects.requireNonNull(vehicle);
this.depot = Objects.requireNonNull(depot);
this.visits = new ArrayList<>(Objects.requireNonNull(visits));
// TODO Probably remove this check when we have more types: new Route(Depot depot, List visits).
// Then visits obviously cannot contain the depot. But will we still require that no visit has the same
// location as the depot? (I don't think so).
if (visits.contains(depot)) {
throw new IllegalArgumentException("Depot (" + depot + ") must not be one of the visits (" + visits + ")");
}
long uniqueVisits = visits.stream().distinct().count();
if (uniqueVisits < visits.size()) {
long duplicates = visits.size() - uniqueVisits;
throw new IllegalArgumentException("Some visits have been visited multiple times (" + duplicates + ")");
}
}
/**
* The vehicle assigned to this route.
*
* @return route's vehicle (never {@code null})
*/
public Vehicle vehicle() {
return vehicle;
}
/**
* Depot in which the route starts and ends.
*
* @return route's depot (never {@code null})
*/
public Location depot() {
return depot;
}
/**
* List of vehicle's visits (not including the depot).
*
* @return list of visits
*/
public List visits() {
return Collections.unmodifiableList(visits);
}
@Override
public String toString() {
return "Route{" +
"vehicle=" + vehicle +
", depot=" + depot.id() +
", visits=" + visits.stream().map(Location::id).collect(toList()) +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/RouteWithTrack.java
================================================
package org.optaweb.vehiclerouting.domain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Vehicle's {@link Route itinerary} enriched with detailed geographical description of the route.
* This object contains data needed to visualize vehicle's route on a map.
*/
public class RouteWithTrack extends Route {
private final List> track;
/**
* Create a route with track. When route is empty (no visits), track must be empty too and vice-versa
* (non-empty route must have a non-empty track).
*
* @param route vehicle's route (not {@code null})
* @param track track going through all visits (not {@code null})
*/
public RouteWithTrack(Route route, List> track) {
super(route.vehicle(), route.depot(), route.visits());
this.track = new ArrayList<>(Objects.requireNonNull(track));
if (route.visits().isEmpty() && !track.isEmpty() || !route.visits().isEmpty() && track.isEmpty()) {
throw new IllegalArgumentException("Route and track must be either both empty or both non-empty");
}
}
/**
* Vehicle's track that goes from vehicle's depot through all visits and returns to the depot.
*
* @return vehicle's track (not {@code null})
*/
public List> track() {
return Collections.unmodifiableList(track);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/RoutingPlan.java
================================================
package org.optaweb.vehiclerouting.domain;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Route plan for the whole vehicle fleet.
*/
public class RoutingPlan {
private static final Logger logger = LoggerFactory.getLogger(RoutingPlan.class);
private final Distance distance;
private final List vehicles;
private final Location depot;
private final List visits;
private final List routes;
/**
* Create a routing plan.
*
* @param distance the overall travel distance
* @param vehicles all available vehicles
* @param depot the depot (may be {@code null})
* @param visits all visits
* @param routes routes of all vehicles
*/
public RoutingPlan(
Distance distance,
List vehicles,
Location depot,
List visits,
List routes) {
this.distance = Objects.requireNonNull(distance);
this.vehicles = new ArrayList<>(Objects.requireNonNull(vehicles));
this.depot = depot;
this.visits = new ArrayList<>(Objects.requireNonNull(visits));
this.routes = new ArrayList<>(Objects.requireNonNull(routes));
if (depot == null) {
if (!routes.isEmpty()) {
throw new IllegalArgumentException("Routes must be empty when depot is null");
}
} else if (routes.size() != vehicles.size()) {
throw new IllegalArgumentException(describeVehiclesRoutesInconsistency(
"There must be exactly one route per vehicle", vehicles, routes));
} else if (haveDifferentVehicles(vehicles, routes)) {
throw new IllegalArgumentException(describeVehiclesRoutesInconsistency(
"Some routes are assigned to non-existent vehicles", vehicles, routes));
} else if (!routes.isEmpty()) {
List visited = routes.stream()
.map(Route::visits)
.flatMap(Collection::stream)
.collect(toList());
ArrayList unvisited = new ArrayList<>(visits);
unvisited.removeAll(visited);
if (!unvisited.isEmpty()) {
// This happens because we're also publishing solutions that are not fully initialized.
// TODO decide whether this allowed or not
logger.warn("Some visits are unvisited: {}", unvisited);
}
visited.removeAll(visits);
if (!visited.isEmpty()) {
throw new IllegalArgumentException(
"Some routes are going through visits that haven't been defined: " + visited);
}
}
}
private static boolean haveDifferentVehicles(List vehicles, List routes) {
return routes.stream()
.map(Route::vehicle)
.anyMatch(vehicle -> !vehicles.contains(vehicle));
}
private static String describeVehiclesRoutesInconsistency(
String cause,
List vehicles,
List extends Route> routes) {
List vehicleIdsFromRoutes = routes.stream()
.map(route -> route.vehicle().id())
.collect(toList());
return cause
+ ":\n- Vehicles (" + vehicles.size() + "): " + vehicles
+ "\n- Routes' vehicleIds (" + routes.size() + "): " + vehicleIdsFromRoutes;
}
/**
* Create an empty routing plan.
*
* @return empty routing plan
*/
public static RoutingPlan empty() {
return new RoutingPlan(Distance.ZERO, emptyList(), null, emptyList(), emptyList());
}
/**
* Total distance traveled (sum of distances of all routes).
*
* @return travel distance
*/
public Distance distance() {
return distance;
}
/**
* All available vehicles.
*
* @return all vehicles
*/
public List vehicles() {
return Collections.unmodifiableList(vehicles);
}
/**
* Routes of all vehicles in the depot. Includes empty routes of vehicles that stay in the depot.
*
* @return all routes (may be empty when there is no depot or no vehicles)
*/
public List routes() {
return Collections.unmodifiableList(routes);
}
/**
* All visits that are part of the routing problem.
*
* @return all visits
*/
public List visits() {
return Collections.unmodifiableList(visits);
}
/**
* The depot.
*
* @return depot (may be missing)
*/
public Optional depot() {
return Optional.ofNullable(depot);
}
/**
* Routing plan is empty when there is no depot, no vehicles and no routes.
*
* @return {@code true} if the plan is empty
*/
public boolean isEmpty() {
// No need to check routes. No depot => no routes.
return depot == null && vehicles.isEmpty();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/RoutingProblem.java
================================================
package org.optaweb.vehiclerouting.domain;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Definition of the vehicle routing problem instance.
*/
public class RoutingProblem {
private final String name;
private final List vehicles;
private final LocationData depot;
private final List visits;
/**
* Create routing problem instance.
*
* @param name the instance name
* @param vehicles list of vehicles (not {@code null})
* @param depot the depot (may be {@code null} if there is no depot)
* @param visits list of visits (not {@code null})
*/
public RoutingProblem(
String name,
List extends VehicleData> vehicles,
LocationData depot,
List extends LocationData> visits) {
this.name = Objects.requireNonNull(name);
this.vehicles = new ArrayList<>(Objects.requireNonNull(vehicles));
this.depot = depot;
this.visits = new ArrayList<>(Objects.requireNonNull(visits));
}
/**
* Get routing problem instance name.
*
* @return routing problem instance name
*/
public String name() {
return name;
}
/**
* Get the depot.
*
* @return depot (never {@code null})
*/
public Optional depot() {
return Optional.ofNullable(depot);
}
/**
* Get locations that should be visited.
*
* @return visits
*/
public List visits() {
return visits;
}
/**
* Vehicles that are part of the problem definition.
*
* @return vehicles
*/
public List vehicles() {
return vehicles;
}
@Override
public String toString() {
return name;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/Vehicle.java
================================================
package org.optaweb.vehiclerouting.domain;
/**
* Vehicle that can be used to deliver cargo to visits.
*/
public class Vehicle extends VehicleData {
private final long id;
Vehicle(long id, String name, int capacity) {
super(name, capacity);
this.id = id;
}
/**
* Vehicle's ID.
*
* @return unique ID
*/
public long id() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Vehicle vehicle = (Vehicle) o;
return id == vehicle.id;
}
@Override
public int hashCode() {
return Long.hashCode(id);
}
@Override
public String toString() {
return name().isEmpty() ? Long.toString(id) : (id + ": '" + name() + "'");
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/VehicleData.java
================================================
package org.optaweb.vehiclerouting.domain;
import java.util.Objects;
/**
* Data about a vehicle.
*/
public class VehicleData {
private final String name;
private final int capacity;
VehicleData(String name, int capacity) {
this.name = Objects.requireNonNull(name);
this.capacity = capacity;
}
/**
* Vehicle's name (unique description).
*
* @return vehicle's name
*/
public String name() {
return name;
}
/**
* Vehicle's capacity.
*
* @return vehicle's capacity
*/
public int capacity() {
return capacity;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
VehicleData that = (VehicleData) o;
return capacity == that.capacity &&
name.equals(that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, capacity);
}
@Override
public String toString() {
return name.isEmpty() ? "" : "'" + name + "'";
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/VehicleFactory.java
================================================
package org.optaweb.vehiclerouting.domain;
/**
* Creates {@link Vehicle} instances.
*/
public class VehicleFactory {
private VehicleFactory() {
throw new AssertionError("Utility class");
}
/**
* Create vehicle data.
*
* @param name vehicle's name
* @param capacity vehicle's capacity
* @return vehicle data
*/
public static VehicleData vehicleData(String name, int capacity) {
return new VehicleData(name, capacity);
}
/**
* Create a new vehicle with the given ID, name and capacity.
*
* @param id vehicle's ID
* @param name vehicle's name
* @param capacity vehicle's capacity
* @return new vehicle
*/
public static Vehicle createVehicle(long id, String name, int capacity) {
return new Vehicle(id, name, capacity);
}
/**
* Create a vehicle with given ID and capacity of zero. The vehicle will have a non-empty name.
*
* @param id vehicle's ID
* @return new testing vehicle instance
*/
public static Vehicle testVehicle(long id) {
return new Vehicle(id, "Vehicle " + id, 0);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/package-info.java
================================================
/**
* Domain model. Contains vehicles, depots, visits and so on.
* The code in this package only depends on {@link java.lang} and is not affected
* by any framework used in this project.
*/
package org.optaweb.vehiclerouting.domain;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceCrudRepository.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import javax.enterprise.context.ApplicationScoped;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
/**
* Distance repository.
*/
@ApplicationScoped
public class DistanceCrudRepository implements PanacheRepositoryBase {
void deleteByFromIdOrToId(long deletedLocationId) {
delete(
"fromId = :deletedLocationId or toId = :deletedLocationId",
Parameters.with("deletedLocationId", deletedLocationId));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceEntity.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import java.util.Objects;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
/**
* Distance between two locations that can be persisted.
*/
@Entity
class DistanceEntity {
@EmbeddedId
private DistanceKey key;
private Long distance;
protected DistanceEntity() {
// for JPA
}
DistanceEntity(DistanceKey key, Long distance) {
this.key = Objects.requireNonNull(key);
this.distance = Objects.requireNonNull(distance);
}
DistanceKey getKey() {
return key;
}
Long getDistance() {
return distance;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DistanceEntity that = (DistanceEntity) o;
return key.equals(that.key) &&
distance.equals(that.distance);
}
@Override
public int hashCode() {
return Objects.hash(key, distance);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceKey.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import java.io.Serializable;
import java.util.Objects;
import javax.persistence.Embeddable;
/**
* Composite key for {@link DistanceEntity}.
*/
@Embeddable
class DistanceKey implements Serializable {
// TODO make it a foreign key to LocationEntity
private Long fromId;
private Long toId;
protected DistanceKey() {
// for JPA
}
DistanceKey(long fromId, long toId) {
this.fromId = fromId;
this.toId = toId;
}
Long getFromId() {
return fromId;
}
void setFromId(Long fromId) {
this.fromId = fromId;
}
Long getToId() {
return toId;
}
void setToId(Long toId) {
this.toId = toId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DistanceKey that = (DistanceKey) o;
return fromId.equals(that.fromId) &&
toId.equals(that.toId);
}
@Override
public int hashCode() {
return Objects.hash(fromId, toId);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceRepositoryImpl.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.service.distance.DistanceRepository;
@ApplicationScoped
class DistanceRepositoryImpl implements DistanceRepository {
private final DistanceCrudRepository distanceRepository;
@Inject
DistanceRepositoryImpl(DistanceCrudRepository distanceRepository) {
this.distanceRepository = distanceRepository;
}
@Override
public void saveDistance(Location from, Location to, Distance distance) {
DistanceEntity distanceEntity = new DistanceEntity(new DistanceKey(from.id(), to.id()), distance.millis());
distanceRepository.persist(distanceEntity);
}
@Override
public Optional getDistance(Location from, Location to) {
return distanceRepository.findByIdOptional(new DistanceKey(from.id(), to.id()))
.map(DistanceEntity::getDistance)
.map(Distance::ofMillis);
}
@Override
public void deleteDistances(Location location) {
distanceRepository.deleteByFromIdOrToId(location.id());
}
@Override
public void deleteAll() {
distanceRepository.deleteAll();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/LocationCrudRepository.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import javax.enterprise.context.ApplicationScoped;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
/**
* Location repository.
*/
@ApplicationScoped
public class LocationCrudRepository implements PanacheRepository {
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/LocationEntity.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import java.math.BigDecimal;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* Persistable location.
*/
@Entity
class LocationEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
// https://wiki.openstreetmap.org/wiki/Node#Structure
@Column(precision = 9, scale = 7)
private BigDecimal latitude;
@Column(precision = 10, scale = 7)
private BigDecimal longitude;
private String description;
protected LocationEntity() {
// for JPA
}
LocationEntity(long id, BigDecimal latitude, BigDecimal longitude, String description) {
this.id = id;
this.latitude = Objects.requireNonNull(latitude);
this.longitude = Objects.requireNonNull(longitude);
this.description = Objects.requireNonNull(description);
}
long getId() {
return id;
}
BigDecimal getLatitude() {
return latitude;
}
BigDecimal getLongitude() {
return longitude;
}
String getDescription() {
return description;
}
@Override
public String toString() {
return "LocationEntity{" +
"id=" + id +
", latitude=" + latitude +
", longitude=" + longitude +
", description='" + description + '\'' +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/LocationRepositoryImpl.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static java.util.stream.Collectors.toList;
import java.util.List;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.service.location.LocationRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
class LocationRepositoryImpl implements LocationRepository {
private static final Logger logger = LoggerFactory.getLogger(LocationRepositoryImpl.class);
private final LocationCrudRepository repository;
@Inject
LocationRepositoryImpl(LocationCrudRepository repository) {
this.repository = repository;
}
@Override
public Location createLocation(Coordinates coordinates, String description) {
LocationEntity locationEntity = new LocationEntity(0, coordinates.latitude(), coordinates.longitude(), description);
repository.persist(locationEntity);
Location location = toDomain(locationEntity);
logger.info("Created location {}.", location.fullDescription());
return location;
}
@Override
public List locations() {
return repository.streamAll()
.map(LocationRepositoryImpl::toDomain)
.collect(toList());
}
@Override
public Location removeLocation(long id) {
Optional maybeLocation = repository.findByIdOptional(id);
maybeLocation.ifPresent(locationEntity -> repository.deleteById(id));
LocationEntity locationEntity = maybeLocation.orElseThrow(
() -> new IllegalArgumentException("Location{id=" + id + "} doesn't exist"));
Location location = toDomain(locationEntity);
logger.info("Deleted location {}.", location.fullDescription());
return location;
}
@Override
public void removeAll() {
repository.deleteAll();
}
@Override
public Optional find(long locationId) {
return repository.findByIdOptional(locationId).map(LocationRepositoryImpl::toDomain);
}
private static Location toDomain(LocationEntity locationEntity) {
return new Location(
locationEntity.getId(),
new Coordinates(locationEntity.getLatitude(), locationEntity.getLongitude()),
locationEntity.getDescription());
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleCrudRepository.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import javax.enterprise.context.ApplicationScoped;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
/**
* Vehicle repository.
*/
@ApplicationScoped
public class VehicleCrudRepository implements PanacheRepository {
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleEntity.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* Persistable vehicle.
*/
@Entity
public class VehicleEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
private int capacity;
protected VehicleEntity() {
// for JPA
}
public VehicleEntity(long id, String name, int capacity) {
this.id = id;
this.name = name;
this.capacity = capacity;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
@Override
public String toString() {
return "VehicleEntity{" +
"id=" + id +
", name='" + name + '\'' +
", capacity=" + capacity +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleRepositoryImpl.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static java.util.stream.Collectors.toList;
import java.util.List;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.domain.VehicleData;
import org.optaweb.vehiclerouting.domain.VehicleFactory;
import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
public class VehicleRepositoryImpl implements VehicleRepository {
private static final Logger logger = LoggerFactory.getLogger(VehicleRepositoryImpl.class);
private final VehicleCrudRepository repository;
@Inject
public VehicleRepositoryImpl(VehicleCrudRepository repository) {
this.repository = repository;
}
@Override
public Vehicle createVehicle(int capacity) {
VehicleEntity vehicleEntity = new VehicleEntity(0, null, capacity);
repository.persist(vehicleEntity);
vehicleEntity.setName("Vehicle " + vehicleEntity.getId());
Vehicle vehicle = toDomain(vehicleEntity);
logger.info("Created vehicle {}.", vehicle);
return vehicle;
}
@Override
public Vehicle createVehicle(VehicleData vehicleData) {
VehicleEntity vehicleEntity = new VehicleEntity(0, vehicleData.name(), vehicleData.capacity());
repository.persist(vehicleEntity);
Vehicle vehicle = toDomain(vehicleEntity);
logger.info("Created vehicle {}.", vehicle);
return vehicle;
}
@Override
public List vehicles() {
return repository.streamAll()
.map(VehicleRepositoryImpl::toDomain)
.collect(toList());
}
@Override
public Vehicle removeVehicle(long id) {
Optional optionalVehicleEntity = repository.findByIdOptional(id);
VehicleEntity vehicleEntity = optionalVehicleEntity.orElseThrow(
() -> new IllegalArgumentException("Vehicle{id=" + id + "} doesn't exist"));
repository.deleteById(id);
Vehicle vehicle = toDomain(vehicleEntity);
logger.info("Deleted vehicle {}.", vehicle);
return vehicle;
}
@Override
public void removeAll() {
repository.deleteAll();
}
@Override
public Optional find(long vehicleId) {
return repository.findByIdOptional(vehicleId).map(VehicleRepositoryImpl::toDomain);
}
@Override
public Vehicle changeCapacity(long vehicleId, int capacity) {
VehicleEntity vehicleEntity = repository.findByIdOptional(vehicleId).orElseThrow(() -> new IllegalArgumentException(
"Can't change Vehicle{id=" + vehicleId + "} because it doesn't exist"));
vehicleEntity.setCapacity(capacity);
repository.flush();
return VehicleFactory.createVehicle(vehicleEntity.getId(), vehicleEntity.getName(), vehicleEntity.getCapacity());
}
private static Vehicle toDomain(VehicleEntity vehicleEntity) {
return VehicleFactory.createVehicle(
vehicleEntity.getId(),
vehicleEntity.getName(),
vehicleEntity.getCapacity());
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/package-info.java
================================================
/**
* Persistence infrastructure.
*/
package org.optaweb.vehiclerouting.plugin.persistence;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/Constants.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
public class Constants {
public static final String SOLVER_CONFIG = "solverConfig.xml";
private Constants() {
throw new AssertionError("Constants class");
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/DistanceMapImpl.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import java.util.Objects;
import org.optaweb.vehiclerouting.plugin.planner.domain.DistanceMap;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation;
import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow;
/**
* Provides distances to {@link PlanningLocation}s by reading from a {@link DistanceMatrixRow}.
*/
public class DistanceMapImpl implements DistanceMap {
private final DistanceMatrixRow distanceMatrixRow;
public DistanceMapImpl(DistanceMatrixRow distanceMatrixRow) {
this.distanceMatrixRow = Objects.requireNonNull(distanceMatrixRow);
}
@Override
public long distanceTo(PlanningLocation location) {
return distanceMatrixRow.distanceTo(location.getId()).millis();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/RouteChangedEventPublisher.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import org.optaweb.vehiclerouting.service.route.RouteChangedEvent;
import org.optaweb.vehiclerouting.service.route.ShallowRoute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converts planning solution to a {@link RouteChangedEvent} and publishes it so that it can be processed by other
* components that listen for this type of event.
*/
@ApplicationScoped
class RouteChangedEventPublisher {
private static final Logger logger = LoggerFactory.getLogger(RouteChangedEventPublisher.class);
private final Event eventPublisher;
@Inject
RouteChangedEventPublisher(Event eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* Publish solution as a {@link RouteChangedEvent}.
*
* @param solution solution
*/
void publishSolution(VehicleRoutingSolution solution) {
RouteChangedEvent event = solutionToEvent(solution, this);
logger.info(
"New solution with {} depots, {} vehicles, {} visits, distance: {}, score: {}",
solution.getDepotList().size(),
solution.getVehicleList().size(),
solution.getVisitList().size(),
event.distance(),
solution.getScore());
logger.debug("Routes: {}", event.routes());
eventPublisher.fire(event);
}
/**
* Convert a planning domain solution to an event that can be published.
*
* @param solution solution
* @param source source of the event
* @return new event describing the solution
*/
static RouteChangedEvent solutionToEvent(VehicleRoutingSolution solution, Object source) {
List routes = routes(solution);
return new RouteChangedEvent(
source,
// Turn negative soft score into a positive amount of time.
Distance.ofMillis(-solution.getScore().softScore()),
vehicleIds(solution),
depotId(solution),
visitIds(solution),
routes);
}
private static List visitIds(VehicleRoutingSolution solution) {
return solution.getVisitList().stream()
.map(visit -> visit.getLocation().getId())
.collect(toList());
}
/**
* Extract routes from the solution. Includes empty routes of vehicles that stay in the depot.
*
* @param solution solution
* @return one route per vehicle
*/
private static List routes(VehicleRoutingSolution solution) {
// TODO include unconnected customers in the result
if (solution.getDepotList().isEmpty()) {
return Collections.emptyList();
}
ArrayList routes = new ArrayList<>();
for (PlanningVehicle vehicle : solution.getVehicleList()) {
PlanningDepot depot = vehicle.getDepot();
if (depot == null) {
throw new IllegalArgumentException(
"Vehicle (id=" + vehicle.getId() + ") is not in the depot. That's not allowed");
}
List visits = new ArrayList<>();
for (PlanningVisit visit : vehicle.getFutureVisits()) {
if (!solution.getVisitList().contains(visit)) {
throw new IllegalArgumentException("Visit (" + visit + ") doesn't exist");
}
visits.add(visit.getLocation().getId());
}
routes.add(new ShallowRoute(vehicle.getId(), depot.getId(), visits));
}
return routes;
}
/**
* Get IDs of vehicles in the solution.
*
* @param solution the solution
* @return vehicle IDs
*/
private static List vehicleIds(VehicleRoutingSolution solution) {
return solution.getVehicleList().stream()
.map(PlanningVehicle::getId)
.collect(toList());
}
/**
* Get solution's depot ID.
*
* @param solution the solution in which to look for the depot
* @return first depot ID from the solution or {@code null} if there are no depots
*/
private static Long depotId(VehicleRoutingSolution solution) {
return solution.getDepotList().isEmpty() ? null : solution.getDepotList().get(0).getId();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/RouteOptimizerConfig.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
/**
* Configuration bean that creates {@link RouteOptimizerImpl route optimizer}'s dependencies.
*/
@Dependent
class RouteOptimizerConfig {
private final SolverFactory solverFactory;
RouteOptimizerConfig(SolverFactory solverFactory) {
this.solverFactory = solverFactory;
}
@Produces
Solver solver() {
return solverFactory.buildSolver();
}
@Produces
ListeningExecutorService executor() {
ExecutorService executorService = Executors.newFixedThreadPool(1);
return MoreExecutors.listeningDecorator(executorService);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/RouteOptimizerImpl.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory;
import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow;
import org.optaweb.vehiclerouting.service.location.LocationPlanner;
import org.optaweb.vehiclerouting.service.vehicle.VehiclePlanner;
/**
* Accumulates vehicles, depots and visits until there's enough data to start the optimization.
* Solutions are published even if solving hasn't started yet due to missing facts (e.g. no vehicles or no visits).
* Stops solver when vehicles or visits are reduced to zero.
*/
@ApplicationScoped
class RouteOptimizerImpl implements LocationPlanner, VehiclePlanner {
private final SolverManager solverManager;
private final RouteChangedEventPublisher routeChangedEventPublisher;
private final List vehicles = new ArrayList<>();
private final List visits = new ArrayList<>();
private PlanningDepot depot;
@Inject
RouteOptimizerImpl(SolverManager solverManager, RouteChangedEventPublisher routeChangedEventPublisher) {
this.solverManager = solverManager;
this.routeChangedEventPublisher = routeChangedEventPublisher;
}
@Override
public void addLocation(Location domainLocation, DistanceMatrixRow distanceMatrixRow) {
PlanningLocation location = PlanningLocationFactory.fromDomain(
domainLocation,
new DistanceMapImpl(distanceMatrixRow));
// Unfortunately can't start solver with an empty solution (see https://issues.redhat.com/browse/PLANNER-776)
if (depot == null) {
depot = new PlanningDepot(location);
publishSolution();
} else {
PlanningVisit visit = PlanningVisitFactory.fromLocation(location);
visits.add(visit);
if (vehicles.isEmpty()) {
publishSolution();
} else if (visits.size() == 1) {
solverManager.startSolver(SolutionFactory.solutionFromVisits(vehicles, depot, visits));
} else {
solverManager.addVisit(visit);
}
}
}
@Override
public void removeLocation(Location domainLocation) {
if (visits.isEmpty()) {
if (depot == null) {
throw new IllegalArgumentException(
"Cannot remove " + domainLocation + " because there are no locations");
}
if (depot.getId() != domainLocation.id()) {
throw new IllegalArgumentException("Cannot remove " + domainLocation + " because it doesn't exist");
}
depot = null;
publishSolution();
} else {
if (depot.getId() == domainLocation.id()) {
throw new IllegalStateException("You can only remove depot if there are no visits");
}
if (!visits.removeIf(item -> item.getId() == domainLocation.id())) {
throw new IllegalArgumentException("Cannot remove " + domainLocation + " because it doesn't exist");
}
if (vehicles.isEmpty()) { // solver is not running
publishSolution();
} else if (visits.isEmpty()) { // solver is running
solverManager.stopSolver();
publishSolution();
} else {
// TODO maybe allow removing location by ID (only require the necessary information)
solverManager.removeVisit(
PlanningVisitFactory.fromLocation(PlanningLocationFactory.fromDomain(domainLocation)));
}
}
}
@Override
public void addVehicle(Vehicle domainVehicle) {
PlanningVehicle vehicle = PlanningVehicleFactory.fromDomain(domainVehicle);
vehicle.setDepot(depot);
vehicles.add(vehicle);
if (visits.isEmpty()) {
publishSolution();
} else if (vehicles.size() == 1) {
solverManager.startSolver(SolutionFactory.solutionFromVisits(vehicles, depot, visits));
} else {
solverManager.addVehicle(vehicle);
}
}
@Override
public void removeVehicle(Vehicle domainVehicle) {
if (!vehicles.removeIf(vehicle -> vehicle.getId() == domainVehicle.id())) {
throw new IllegalArgumentException("Cannot remove " + domainVehicle + " because it doesn't exist");
}
if (visits.isEmpty()) { // solver is not running
publishSolution();
} else if (vehicles.isEmpty()) { // solver is running
solverManager.stopSolver();
publishSolution();
} else {
solverManager.removeVehicle(PlanningVehicleFactory.fromDomain(domainVehicle));
}
}
@Override
public void changeCapacity(Vehicle domainVehicle) {
PlanningVehicle vehicle = vehicles.stream()
.filter(item -> item.getId() == domainVehicle.id())
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"Cannot change capacity of " + domainVehicle + " because it doesn't exist"));
vehicle.setCapacity(domainVehicle.capacity());
if (!visits.isEmpty()) {
solverManager.changeCapacity(vehicle);
} else {
publishSolution();
}
}
@Override
public void removeAllLocations() {
solverManager.stopSolver();
depot = null;
visits.clear();
publishSolution();
}
@Override
public void removeAllVehicles() {
solverManager.stopSolver();
vehicles.clear();
publishSolution();
}
private void publishSolution() {
routeChangedEventPublisher.publishSolution(SolutionFactory.solutionFromVisits(vehicles, depot, visits));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/SolverManager.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Default;
import javax.inject.Inject;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent;
import org.optaplanner.core.api.solver.event.SolverEventListener;
import org.optaweb.vehiclerouting.plugin.planner.change.AddVehicle;
import org.optaweb.vehiclerouting.plugin.planner.change.AddVisit;
import org.optaweb.vehiclerouting.plugin.planner.change.ChangeVehicleCapacity;
import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVehicle;
import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import org.optaweb.vehiclerouting.service.error.ErrorEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
/**
* Manages a solver running in a different thread.
*
* Does following:
*
* - Starts solver by running {@link Solver#solve(Object problem)} in a thread that's not the caller's thread.
* - Stops the solver (synchronously).
* - Adds problem fact changes to the solver.
* - Propagates any exception that happens in {@code Solver.solver()} (in a different thread) to the thread that
* interacts with {@code SolverManager}.
* - Listens for best solution changes and publishes new best solutions via {@link RouteChangedEventPublisher}.
*
*/
@ApplicationScoped
@Default
class SolverManager implements SolverEventListener {
private static final Logger logger = LoggerFactory.getLogger(SolverManager.class);
private final Solver solver;
private final ListeningExecutorService executor;
private final RouteChangedEventPublisher routeChangedEventPublisher;
private final Event errorEvent;
private ListenableFuture solverFuture;
@Inject
SolverManager(
Solver solver,
ListeningExecutorService executor,
RouteChangedEventPublisher routeChangedEventPublisher,
Event errorEvent) {
this.solver = solver;
this.executor = executor;
this.routeChangedEventPublisher = routeChangedEventPublisher;
this.errorEvent = errorEvent;
this.solver.addEventListener(this);
}
@Override
public void bestSolutionChanged(BestSolutionChangedEvent bestSolutionChangedEvent) {
// CAUTION! This runs on the solver thread. Implications:
// 1. The method should be as quick as possible to avoid blocking solver unnecessarily.
// 2. This place is a potential source of race conditions.
if (!bestSolutionChangedEvent.isEveryProblemChangeProcessed()) {
logger.info("Ignoring a new best solution that has some problem facts missing");
return;
}
// TODO Race condition, if a servlet thread deletes that location in the middle of this method happening
// on the solver thread. Make sure that location is still in the repository.
// Maybe repair the solution OR ignore if it's inconsistent (log a WARNING).
routeChangedEventPublisher.publishSolution(bestSolutionChangedEvent.getNewBestSolution()); // TODO @Async
}
void startSolver(VehicleRoutingSolution solution) {
if (solverFuture != null) {
throw new IllegalStateException("Solver start has already been requested");
}
solverFuture = executor.submit((SolvingTask) () -> solver.solve(solution));
solverFuture.addListener(
// IMPORTANT: This is happening on the solver thread.
// TODO maybe restart or somehow recover?
() -> {
if (!solver.isTerminateEarly()) {
// Solver in daemon mode can't return from solve() unless it has been terminated early
// (see #stopSolver()).
// So this case is only possible when an exception is thrown during solver.solve().
try {
solverFuture.get();
logger.error("The solver has stopped without being terminated early so at this point"
+ " it is expected to have crashed but there was no exception.\n"
+ "If you see this other than during test execution it is probably a bug.");
errorEvent.fire(new ErrorEvent(
this,
"Solver stopped without being terminated early and without throwing an exception."
+ " This is a bug."));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while retrieving the cause of solver failure", e);
} catch (ExecutionException e) {
logger.error("Solver failed", e);
errorEvent.fire(new ErrorEvent(this, e.toString()));
}
}
},
MoreExecutors.directExecutor());
}
void stopSolver() {
if (solverFuture != null) {
// TODO what happens if solver hasn't started yet (solve() is called asynchronously)
solver.terminateEarly();
// make sure solver has terminated and propagate exceptions
try {
solverFuture.get();
solverFuture = null;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Failed to stop solver", e);
} catch (ExecutionException e) {
// Skipping the wrapper ExecutionException because it only tells that the problem occurred
// in solverFuture.get() but that's obvious.
throw new RuntimeException("Failed to stop solver", e.getCause());
}
}
}
private void assertSolverIsAlive() {
if (solverFuture == null) {
throw new IllegalStateException("Solver has not started yet");
}
if (solverFuture.isDone()) {
try {
solverFuture.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Solver has died", e);
} catch (ExecutionException e) {
// Skipping the wrapper ExecutionException because it only tells that the problem occurred
// in solverFuture.get() but that's obvious.
throw new RuntimeException("Solver has died", e.getCause());
}
throw new IllegalStateException("Solver has finished solving even though it operates in daemon mode");
}
}
void addVisit(PlanningVisit visit) {
assertSolverIsAlive();
solver.addProblemChange(new AddVisit(visit));
}
void removeVisit(PlanningVisit visit) {
assertSolverIsAlive();
solver.addProblemChange(new RemoveVisit(visit));
}
void addVehicle(PlanningVehicle vehicle) {
assertSolverIsAlive();
solver.addProblemChange(new AddVehicle(vehicle));
}
void removeVehicle(PlanningVehicle vehicle) {
assertSolverIsAlive();
solver.addProblemChange(new RemoveVehicle(vehicle));
}
void changeCapacity(PlanningVehicle vehicle) {
assertSolverIsAlive();
solver.addProblemChange(new ChangeVehicleCapacity(vehicle));
}
/**
* An alias interface that fixates the Callable's type parameter. This avoids unchecked warnings in tests.
*/
interface SolvingTask extends Callable {
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/VehicleRoutingConstraintProvider.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static org.optaplanner.core.api.score.stream.ConstraintCollectors.sum;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
public class VehicleRoutingConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
vehicleCapacity(constraintFactory),
distanceFromPreviousStandstill(constraintFactory),
distanceFromLastVisitToDepot(constraintFactory)
};
}
Constraint vehicleCapacity(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(PlanningVisit.class)
.groupBy(PlanningVisit::getVehicle, sum(PlanningVisit::getDemand))
.filter((vehicle, demand) -> demand > vehicle.getCapacity())
.penalizeLong(HardSoftLongScore.ONE_HARD,
(vehicle, demand) -> demand - vehicle.getCapacity())
.asConstraint("vehicle capacity");
}
Constraint distanceFromPreviousStandstill(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(PlanningVisit.class)
.penalizeLong(HardSoftLongScore.ONE_SOFT,
PlanningVisit::distanceFromPreviousStandstill)
.asConstraint("distance from previous standstill");
}
Constraint distanceFromLastVisitToDepot(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(PlanningVisit.class)
.filter(PlanningVisit::isLast)
.penalizeLong(HardSoftLongScore.ONE_SOFT,
PlanningVisit::distanceToDepot)
.asConstraint("distance from last visit to depot");
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/AddVehicle.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.change;
import java.util.Objects;
import org.optaplanner.core.api.solver.change.ProblemChange;
import org.optaplanner.core.api.solver.change.ProblemChangeDirector;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
public class AddVehicle implements ProblemChange {
private final PlanningVehicle vehicle;
public AddVehicle(PlanningVehicle vehicle) {
this.vehicle = Objects.requireNonNull(vehicle);
}
@Override
public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) {
problemChangeDirector.addProblemFact(vehicle, workingSolution.getVehicleList()::add);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/AddVisit.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.change;
import java.util.Objects;
import org.optaplanner.core.api.solver.change.ProblemChange;
import org.optaplanner.core.api.solver.change.ProblemChangeDirector;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
public class AddVisit implements ProblemChange {
private final PlanningVisit visit;
public AddVisit(PlanningVisit visit) {
this.visit = Objects.requireNonNull(visit);
}
@Override
public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) {
problemChangeDirector.addEntity(visit, workingSolution.getVisitList()::add);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/ChangeVehicleCapacity.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.change;
import java.util.Objects;
import org.optaplanner.core.api.solver.change.ProblemChange;
import org.optaplanner.core.api.solver.change.ProblemChangeDirector;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
public class ChangeVehicleCapacity implements ProblemChange {
private final PlanningVehicle vehicle;
public ChangeVehicleCapacity(PlanningVehicle vehicle) {
this.vehicle = Objects.requireNonNull(vehicle);
}
@Override
public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) {
// No need to clone the workingVehicle because it is a planning entity, so it is already planning-cloned.
// To learn more about problem fact changes, see:
// https://www.optaplanner.org/docs/optaplanner/latest/repeated-planning/repeated-planning.html#problemChangeExample
problemChangeDirector.changeProblemProperty(vehicle,
workingVehicle -> workingVehicle.setCapacity(vehicle.getCapacity()));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/RemoveVehicle.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.change;
import java.util.Objects;
import org.optaplanner.core.api.solver.change.ProblemChange;
import org.optaplanner.core.api.solver.change.ProblemChangeDirector;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
public class RemoveVehicle implements ProblemChange {
private final PlanningVehicle removedVehicle;
public RemoveVehicle(PlanningVehicle removedVehicle) {
this.removedVehicle = Objects.requireNonNull(removedVehicle);
}
@Override
public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) {
// Look up a working copy of the vehicle
PlanningVehicle workingVehicle = problemChangeDirector.lookUpWorkingObjectOrFail(removedVehicle);
// Un-initialize all visits of this vehicle
for (PlanningVisit visit : workingVehicle.getFutureVisits()) {
problemChangeDirector.changeVariable(visit, "previousStandstill",
planningVisit -> planningVisit.setPreviousStandstill(null));
}
// No need to clone the vehicleList because it is a planning entity collection, so it is already
// planning-cloned.
// To learn more about problem fact changes, see:
// https://www.optaplanner.org/docs/optaplanner/latest/repeated-planning/repeated-planning.html#problemChangeExample
// Remove the vehicle
problemChangeDirector.removeProblemFact(workingVehicle, planningVehicle -> {
if (!workingSolution.getVehicleList().remove(planningVehicle)) {
throw new IllegalStateException(
"Working solution's vehicleList "
+ workingSolution.getVehicleList()
+ " doesn't contain the workingVehicle ("
+ planningVehicle
+ "). This is a bug!");
}
});
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/RemoveVisit.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.change;
import java.util.Objects;
import org.optaplanner.core.api.solver.change.ProblemChange;
import org.optaplanner.core.api.solver.change.ProblemChangeDirector;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
public class RemoveVisit implements ProblemChange {
private final PlanningVisit planningVisit;
public RemoveVisit(PlanningVisit planningVisit) {
this.planningVisit = Objects.requireNonNull(planningVisit);
}
@Override
public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) {
// Look up a working copy of the visit
PlanningVisit workingVisit = problemChangeDirector.lookUpWorkingObjectOrFail(planningVisit);
// Fix the next visit and set its previousStandstill to the removed visit's previousStandstill
PlanningVisit nextVisit = workingVisit.getNextVisit();
if (nextVisit != null) { // otherwise it's the last visit
problemChangeDirector.changeVariable(nextVisit, "previousStandstill",
workingNextVisit -> workingNextVisit.setPreviousStandstill(workingVisit.getPreviousStandstill()));
}
// No need to clone the visitList because it is a planning entity collection, so it is already planning-cloned.
// To learn more about problem fact changes, see:
// https://www.optaplanner.org/docs/optaplanner/latest/repeated-planning/repeated-planning.html#problemChangeExample
// Remove the visit
problemChangeDirector.removeEntity(planningVisit, visit -> {
if (!workingSolution.getVisitList().remove(visit)) {
throw new IllegalStateException(
"Working solution's visitList "
+ workingSolution.getVisitList()
+ " doesn't contain the workingVisit ("
+ visit
+ "). This is a bug!");
}
});
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/package-info.java
================================================
/**
* {@link org.optaplanner.core.api.solver.change.ProblemChange} implementations.
*
* Problem fact changes are difficult to write correctly. To understand the code and when implementing new fact changes,
* read
* ProblemChange documentation.
*/
package org.optaweb.vehiclerouting.plugin.planner.change;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/DistanceMap.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
/**
* Contains travel distances from a reference location to other locations.
*/
@FunctionalInterface
public interface DistanceMap {
/**
* Get distance from a reference location to the given location. The actual physical quantity (distance or time)
* and its units depend on the configuration of the routing engine and is not important for optimization.
*
* @param location location the distance of which will be returned
* @return location's distance
*/
long distanceTo(PlanningLocation location);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningDepot.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import java.util.Objects;
public class PlanningDepot {
private final PlanningLocation location;
public PlanningDepot(PlanningLocation location) {
this.location = Objects.requireNonNull(location);
}
public long getId() {
return location.getId();
}
public PlanningLocation getLocation() {
return location;
}
@Override
public String toString() {
return "PlanningDepot{" +
"location=" + location.getId() +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningLocation.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import java.util.Objects;
public class PlanningLocation {
private final long id;
// Only used to calculate angle.
private final double latitude;
private final double longitude;
private final DistanceMap travelDistanceMap;
PlanningLocation(long id, double latitude, double longitude, DistanceMap travelDistanceMap) {
this.id = id;
this.latitude = latitude;
this.longitude = longitude;
this.travelDistanceMap = Objects.requireNonNull(travelDistanceMap);
}
/**
* ID of the corresponding domain location.
*
* @return domain location ID
*/
public long getId() {
return id;
}
/**
* Distance to the given location.
*
* @param location other location
* @return distance to the other location
*/
public long distanceTo(PlanningLocation location) {
if (this == location) {
return 0L;
}
return travelDistanceMap.distanceTo(location);
}
/**
* Angle between the given location and the direction EAST with {@code this} location being the vertex.
*
* @param location location that forms one side of the angle (not {@code null})
* @return angle in radians in the range of -π to π
*/
public double angleTo(PlanningLocation location) {
// Euclidean distance (Pythagorean theorem) - not correct when the surface is a sphere
double latitudeDifference = location.latitude - latitude;
double longitudeDifference = location.longitude - longitude;
return Math.atan2(latitudeDifference, longitudeDifference);
}
@Override
public String toString() {
return "PlanningLocation{" +
"latitude=" + latitude +
",longitude=" + longitude +
",travelDistanceMap=" + travelDistanceMap +
",id=" + id +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningLocationFactory.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import org.optaweb.vehiclerouting.domain.Location;
/**
* Creates {@link PlanningLocation}s.
*/
public class PlanningLocationFactory {
private PlanningLocationFactory() {
throw new AssertionError("Utility class");
}
/**
* Create planning location without a distance map. This location cannot be used for planning but can be used for
* a problem fact change to remove a visit.
*
* @param location domain location
* @return planning location without distance map
*/
public static PlanningLocation fromDomain(Location location) {
return fromDomain(location, PlanningLocationFactory::failFast);
}
/**
* Create planning location from a domain location and a distance map.
*
* @param location domain location
* @param distanceMap distance map of this planning location
* @return planning location
*/
public static PlanningLocation fromDomain(Location location, DistanceMap distanceMap) {
return new PlanningLocation(
location.id(),
location.coordinates().latitude().doubleValue(),
location.coordinates().longitude().doubleValue(),
distanceMap);
}
/**
* Create test location without distance map and coordinates. Coordinates will be initialized to zero.
*
* @param id location ID
* @return planning location without distance map and coordinates
*/
public static PlanningLocation testLocation(long id) {
return testLocation(id, PlanningLocationFactory::failFast);
}
/**
* Create test location with distance map and without coordinates. Coordinates will be initialized to zero.
*
* @param id location ID
* @param distanceMap distance map
* @return planning location with distance map and without coordinates
*/
public static PlanningLocation testLocation(long id, DistanceMap distanceMap) {
return new PlanningLocation(id, 0, 0, distanceMap);
}
private static long failFast(PlanningLocation location) {
throw new IllegalStateException();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVehicle.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.optaplanner.core.api.domain.lookup.PlanningId;
public class PlanningVehicle implements Standstill {
@PlanningId
private long id;
private int capacity;
private PlanningDepot depot;
// Shadow variables
private PlanningVisit nextVisit;
PlanningVehicle() {
// Hide public constructor in favor of the factory.
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public PlanningDepot getDepot() {
return depot;
}
public void setDepot(PlanningDepot depot) {
this.depot = depot;
}
@Override
public PlanningVisit getNextVisit() {
return nextVisit;
}
@Override
public void setNextVisit(PlanningVisit nextVisit) {
this.nextVisit = nextVisit;
}
public Iterable getFutureVisits() {
return () -> new Iterator() {
PlanningVisit nextVisit = getNextVisit();
@Override
public boolean hasNext() {
return nextVisit != null;
}
@Override
public PlanningVisit next() {
if (nextVisit == null) {
throw new NoSuchElementException();
}
PlanningVisit out = nextVisit;
nextVisit = nextVisit.getNextVisit();
return out;
}
};
}
@Override
public PlanningLocation getLocation() {
return depot.getLocation();
}
@Override
public String toString() {
return "PlanningVehicle{" +
"capacity=" + capacity +
(depot == null ? "" : ",depot=" + depot.getId()) +
(nextVisit == null ? "" : ",nextVisit=" + nextVisit.getId()) +
",id=" + id +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVehicleFactory.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import org.optaweb.vehiclerouting.domain.Vehicle;
/**
* Creates {@link PlanningVehicle} instances.
*/
public class PlanningVehicleFactory {
private PlanningVehicleFactory() {
throw new AssertionError("Utility class");
}
/**
* Create planning vehicle from domain vehicle.
*
* @param domainVehicle domain vehicle
* @return planning vehicle
*/
public static PlanningVehicle fromDomain(Vehicle domainVehicle) {
return vehicle(domainVehicle.id(), domainVehicle.capacity());
}
/**
* Create a testing vehicle with zero capacity.
*
* @param id vehicle's ID
* @return new vehicle with zero capacity
*/
public static PlanningVehicle testVehicle(long id) {
return vehicle(id, 0);
}
/**
* Create a testing vehicle with capacity.
*
* @param id vehicle's ID
* @return new vehicle with the given capacity
*/
public static PlanningVehicle testVehicle(long id, int capacity) {
return vehicle(id, capacity);
}
private static PlanningVehicle vehicle(long id, int capacity) {
PlanningVehicle vehicle = new PlanningVehicle();
vehicle.setId(id);
vehicle.setCapacity(capacity);
return vehicle;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVisit.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.lookup.PlanningId;
import org.optaplanner.core.api.domain.variable.AnchorShadowVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariableGraphType;
import org.optaweb.vehiclerouting.plugin.planner.weight.DepotAngleVisitDifficultyWeightFactory;
@PlanningEntity(difficultyWeightFactoryClass = DepotAngleVisitDifficultyWeightFactory.class)
public class PlanningVisit implements Standstill {
@PlanningId
private long id;
private PlanningLocation location;
private int demand;
// Planning variable: changes during planning, between score calculations.
@PlanningVariable(valueRangeProviderRefs = { "vehicleRange", "visitRange" },
graphType = PlanningVariableGraphType.CHAINED)
private Standstill previousStandstill;
// Shadow variables
private PlanningVisit nextVisit;
@AnchorShadowVariable(sourceVariableName = "previousStandstill")
private PlanningVehicle vehicle;
PlanningVisit() {
// Hide public constructor in favor of the factory.
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@Override
public PlanningLocation getLocation() {
return location;
}
public void setLocation(PlanningLocation location) {
this.location = location;
}
public int getDemand() {
return demand;
}
public void setDemand(int demand) {
this.demand = demand;
}
public Standstill getPreviousStandstill() {
return previousStandstill;
}
public void setPreviousStandstill(Standstill previousStandstill) {
this.previousStandstill = previousStandstill;
}
@Override
public PlanningVisit getNextVisit() {
return nextVisit;
}
@Override
public void setNextVisit(PlanningVisit nextVisit) {
this.nextVisit = nextVisit;
}
public PlanningVehicle getVehicle() {
return vehicle;
}
public void setVehicle(PlanningVehicle vehicle) {
this.vehicle = vehicle;
}
// ************************************************************************
// Complex methods
// ************************************************************************
/**
* Distance from the previous standstill to this visit. This is used to calculate the travel cost of a chain
* beginning with a vehicle (at a depot) and ending with the {@link #isLast() last} visit.
* The chain ends with a visit, not a depot so the cost of returning from the last visit back to the depot
* has to be added in a separate step using {@link #distanceToDepot()}.
*
* @return distance from previous standstill to this visit
*/
public long distanceFromPreviousStandstill() {
if (previousStandstill == null) {
throw new IllegalStateException(
"This method must not be called when the previousStandstill (null) is not initialized yet.");
}
return previousStandstill.getLocation().distanceTo(location);
}
/**
* Distance from this visit back to the depot.
*
* @return distance from this visit back its vehicle's depot
*/
public long distanceToDepot() {
return location.distanceTo(vehicle.getLocation());
}
/**
* Whether this visit is the last in a chain.
*
* @return true, if this visit has no {@link #getNextVisit() next} visit
*/
public boolean isLast() {
return nextVisit == null;
}
@Override
public String toString() {
return "PlanningVisit{" +
(location == null ? "" : "location=" + location.getId()) +
",demand=" + demand +
(previousStandstill == null ? "" : ",previousStandstill='" + previousStandstill.getLocation().getId()) +
(nextVisit == null ? "" : ",nextVisit=" + nextVisit.getId()) +
(vehicle == null ? "" : ",vehicle=" + vehicle.getId()) +
",id=" + id +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVisitFactory.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
/**
* Creates {@link PlanningVisit} instances.
*/
public class PlanningVisitFactory {
static final int DEFAULT_VISIT_DEMAND = 1;
private PlanningVisitFactory() {
throw new AssertionError("Utility class");
}
/**
* Create visit with {@link #DEFAULT_VISIT_DEMAND}.
*
* @param location visit's location
* @return new visit with the default demand
*/
public static PlanningVisit fromLocation(PlanningLocation location) {
return fromLocation(location, DEFAULT_VISIT_DEMAND);
}
/**
* Create visit of a location with the given demand.
*
* @param location visit's location
* @param demand visit's demand
* @return visit with demand at the given location
*/
public static PlanningVisit fromLocation(PlanningLocation location, int demand) {
PlanningVisit visit = new PlanningVisit();
visit.setId(location.getId());
visit.setLocation(location);
visit.setDemand(demand);
return visit;
}
/**
* Create a test visit with the given ID.
*
* @param id ID of the visit and its location
* @return visit with an ID only
*/
public static PlanningVisit testVisit(long id) {
return fromLocation(PlanningLocationFactory.testLocation(id));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/SolutionFactory.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import java.util.ArrayList;
import java.util.List;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
/**
* Creates {@link VehicleRoutingSolution} instances.
*/
public class SolutionFactory {
private SolutionFactory() {
throw new AssertionError("Utility class");
}
/**
* Create an empty solution. Empty solution has zero locations, depots, visits and vehicles and a zero score.
*
* @return empty solution
*/
public static VehicleRoutingSolution emptySolution() {
VehicleRoutingSolution solution = new VehicleRoutingSolution();
solution.setVisitList(new ArrayList<>());
solution.setDepotList(new ArrayList<>());
solution.setVehicleList(new ArrayList<>());
solution.setScore(HardSoftLongScore.ZERO);
return solution;
}
/**
* Create a new solution from given vehicles, depot and visits.
* All vehicles will be placed in the depot.
*
* The returned solution's vehicles and locations are new collections so modifying the solution
* won't affect the collections given as arguments.
*
* Elements of the argument collections are NOT cloned.
*
* @param vehicles vehicles
* @param depot depot
* @param visits visits
* @return solution containing the given vehicles, depot, visits and their locations
*/
public static VehicleRoutingSolution solutionFromVisits(
List vehicles,
PlanningDepot depot,
List visits) {
VehicleRoutingSolution solution = new VehicleRoutingSolution();
solution.setVehicleList(new ArrayList<>(vehicles));
solution.setDepotList(new ArrayList<>(1));
if (depot != null) {
solution.getDepotList().add(depot);
moveAllVehiclesToDepot(vehicles, depot);
}
solution.setVisitList(new ArrayList<>(visits));
solution.setScore(HardSoftLongScore.ZERO);
return solution;
}
private static void moveAllVehiclesToDepot(List vehicles, PlanningDepot depot) {
vehicles.forEach(vehicle -> vehicle.setDepot(depot));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/Standstill.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.variable.InverseRelationShadowVariable;
@PlanningEntity
public interface Standstill {
/**
* The standstill's location.
*
* @return never {@code null}
*/
PlanningLocation getLocation();
/**
* The next visit after this standstill.
*
* @return sometimes {@code null}
*/
@InverseRelationShadowVariable(sourceVariableName = "previousStandstill")
PlanningVisit getNextVisit();
void setNextVisit(PlanningVisit nextVisit);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/VehicleRoutingSolution.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.domain;
import java.util.List;
import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
@PlanningSolution
public class VehicleRoutingSolution {
@ProblemFactCollectionProperty
private List depotList;
@PlanningEntityCollectionProperty
@ValueRangeProvider(id = "vehicleRange")
private List vehicleList;
@PlanningEntityCollectionProperty
@ValueRangeProvider(id = "visitRange")
private List visitList;
@PlanningScore
private HardSoftLongScore score;
VehicleRoutingSolution() {
// Hide public constructor in favor of the factory.
}
public List getDepotList() {
return this.depotList;
}
public void setDepotList(List depotList) {
this.depotList = depotList;
}
public List getVehicleList() {
return this.vehicleList;
}
public void setVehicleList(List vehicleList) {
this.vehicleList = vehicleList;
}
public List getVisitList() {
return this.visitList;
}
public void setVisitList(List visitList) {
this.visitList = visitList;
}
public HardSoftLongScore getScore() {
return this.score;
}
public void setScore(HardSoftLongScore score) {
this.score = score;
}
@Override
public String toString() {
return "VehicleRoutingSolution{" +
"depotList=" + depotList +
", vehicleList=" + vehicleList +
", visitList=" + visitList +
", score=" + score +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/package-info.java
================================================
/**
* Domain model adjusted for OptaPlanner.
*/
package org.optaweb.vehiclerouting.plugin.planner.domain;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/package-info.java
================================================
/**
* Route optimization.
*/
package org.optaweb.vehiclerouting.plugin.planner;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/weight/DepotAngleVisitDifficultyWeightFactory.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.weight;
import static java.util.Comparator.comparingDouble;
import static java.util.Comparator.comparingLong;
import java.util.Comparator;
import java.util.Objects;
import org.optaplanner.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
/**
* On large data sets, the constructed solution looks like pizza slices.
* The order of the slices depends on the {@link PlanningLocation#angleTo} implementation.
*/
public class DepotAngleVisitDifficultyWeightFactory
implements SelectionSorterWeightFactory {
@Override
public DepotAngleVisitDifficultyWeight createSorterWeight(VehicleRoutingSolution solution, PlanningVisit visit) {
PlanningDepot depot = solution.getDepotList().get(0);
return new DepotAngleVisitDifficultyWeight(
visit,
// angle of the line from visit to depot relative to visit→east
visit.getLocation().angleTo(depot.getLocation()),
visit.getLocation().distanceTo(depot.getLocation())
+ depot.getLocation().distanceTo(visit.getLocation()));
}
static class DepotAngleVisitDifficultyWeight implements Comparable {
private static final Comparator COMPARATOR =
comparingDouble((DepotAngleVisitDifficultyWeight weight) -> weight.depotAngle)
// Ascending (further from the depot are more difficult)
.thenComparingLong(weight -> weight.depotRoundTripDistance)
.thenComparing(weight -> weight.visit, comparingLong(PlanningVisit::getId));
private final PlanningVisit visit;
private final double depotAngle;
private final long depotRoundTripDistance;
DepotAngleVisitDifficultyWeight(PlanningVisit visit, double depotAngle, long depotRoundTripDistance) {
this.visit = visit;
this.depotAngle = depotAngle;
this.depotRoundTripDistance = depotRoundTripDistance;
}
@Override
public int compareTo(DepotAngleVisitDifficultyWeight other) {
return COMPARATOR.compare(this, other);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DepotAngleVisitDifficultyWeight)) {
return false;
}
return compareTo((DepotAngleVisitDifficultyWeight) o) == 0;
}
@Override
public int hashCode() {
return Objects.hash(visit, depotAngle, depotRoundTripDistance);
}
@Override
public String toString() {
return "DepotAngleVisitDifficultyWeight{" +
"visit=" + visit +
", depotAngle=" + depotAngle +
", depotRoundTripDistance=" + depotRoundTripDistance +
'}';
}
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/weight/package-info.java
================================================
/**
* Implements
*
* planning entity difficulty comparison
*
* or
*
* planning variable strength comparison
*
* to enable advanced
*
* Construction Heuristic
*
* algorithms or
*
* sorted selection
* .
*/
package org.optaweb.vehiclerouting.plugin.planner.weight;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/ClearResource.java
================================================
package org.optaweb.vehiclerouting.plugin.rest;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import org.optaweb.vehiclerouting.service.location.LocationService;
import org.optaweb.vehiclerouting.service.vehicle.VehicleService;
@Path("api/clear")
public class ClearResource {
private final LocationService locationService;
private final VehicleService vehicleService;
@Inject
public ClearResource(LocationService locationService, VehicleService vehicleService) {
this.locationService = locationService;
this.vehicleService = vehicleService;
}
@POST
public void clear() {
// TODO do this in one step (=> new RoutingPlanService)
vehicleService.removeAll();
locationService.removeAll();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/DataSetDownloadResource.java
================================================
package org.optaweb.vehiclerouting.plugin.rest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.optaweb.vehiclerouting.service.demo.DemoService;
/**
* Serves the current data set as a downloadable YAML file.
*/
@Path("api/dataset/export")
@Produces("text/x-yaml")
public class DataSetDownloadResource {
private final DemoService demoService;
DataSetDownloadResource(DemoService demoService) {
this.demoService = demoService;
}
@GET
public Response exportDataSet() throws IOException {
String dataSet = demoService.exportDataSet();
byte[] dataSetBytes = dataSet.getBytes(StandardCharsets.UTF_8);
try (InputStream is = new ByteArrayInputStream(dataSetBytes)) {
return Response.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"vrp_data_set.yaml\"")
.header(HttpHeaders.CONTENT_LENGTH, dataSetBytes.length)
.type(new MediaType("text", "x-yaml", StandardCharsets.UTF_8.name()))
.entity(is)
.build();
}
}
// TODO exception handler
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/DemoResource.java
================================================
package org.optaweb.vehiclerouting.plugin.rest;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.optaweb.vehiclerouting.service.demo.DemoService;
@Path("api/demo/{name}")
public class DemoResource {
private final DemoService demoService;
@Inject
public DemoResource(DemoService demoService) {
this.demoService = demoService;
}
/**
* Load a demo data set.
*
* @param name data set name
*/
@POST
public void loadDemo(@PathParam("name") String name) {
demoService.loadDemo(name);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/LocationResource.java
================================================
package org.optaweb.vehiclerouting.plugin.rest;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.plugin.rest.model.PortableLocation;
import org.optaweb.vehiclerouting.service.location.LocationService;
@Path("api/location")
public class LocationResource {
private final LocationService locationService;
@Inject
public LocationResource(LocationService locationService) {
this.locationService = locationService;
}
/**
* Create new location.
*
* @param request new location description
*/
@Transactional
@POST
public void addLocation(PortableLocation request) {
locationService.createLocation(
new Coordinates(request.getLatitude(), request.getLongitude()),
request.getDescription());
}
/**
* Delete location.
*
* @param id ID of the location to be deleted
*/
@Transactional
@DELETE
@Path("{id}")
public void deleteLocation(@PathParam("id") long id) {
locationService.removeLocation(id);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/RouteEventResource.java
================================================
package org.optaweb.vehiclerouting.plugin.rest;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.sse.OutboundSseEvent;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseBroadcaster;
import javax.ws.rs.sse.SseEventSink;
import org.optaweb.vehiclerouting.domain.RoutingPlan;
import org.optaweb.vehiclerouting.plugin.rest.model.PortableErrorMessage;
import org.optaweb.vehiclerouting.plugin.rest.model.PortableRoutingPlanFactory;
import org.optaweb.vehiclerouting.service.error.ErrorMessage;
import org.optaweb.vehiclerouting.service.route.RouteListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
@Path("api/events")
public class RouteEventResource {
private static final Logger logger = LoggerFactory.getLogger(RouteEventResource.class);
// TODO repository, not listener (service)
private final RouteListener routeListener;
private SseBroadcaster sseBroadcaster;
private OutboundSseEvent.Builder eventBuilder;
@Inject
public RouteEventResource(RouteListener routeListener) {
this.routeListener = routeListener;
}
// Handy during development.
@PreDestroy
public void closeBroadcaster() {
if (sseBroadcaster != null) {
logger.debug("Closing Server-Sent Events broadcaster.");
sseBroadcaster.close();
}
}
public void observeRoute(@Observes RoutingPlan event) {
if (sseBroadcaster != null) {
sseBroadcaster.broadcast(eventBuilder
.data(PortableRoutingPlanFactory.fromRoutingPlan(event))
.name("route")
.comment("route update")
.build());
}
}
public void observeError(@Observes ErrorMessage event) {
if (sseBroadcaster != null) {
sseBroadcaster.broadcast(eventBuilder
.data(PortableErrorMessage.fromMessage(event))
.name("errorMessage")
.comment("error message")
.build());
}
}
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
public void sse(@Context Sse sse, @Context SseEventSink eventSink) {
if (sseBroadcaster == null) {
sseBroadcaster = sse.newBroadcaster();
eventBuilder = sse.newEventBuilder()
.mediaType(MediaType.APPLICATION_JSON_TYPE)
.reconnectDelay(3000);
}
OutboundSseEvent sseEvent = eventBuilder
.data(PortableRoutingPlanFactory.fromRoutingPlan(routeListener.getBestRoutingPlan()))
.comment("best route")
.build();
eventSink.send(sseEvent);
sseBroadcaster.register(eventSink);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/ServerInfoResource.java
================================================
package org.optaweb.vehiclerouting.plugin.rest;
import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.optaweb.vehiclerouting.plugin.rest.model.PortableCoordinates;
import org.optaweb.vehiclerouting.plugin.rest.model.RoutingProblemInfo;
import org.optaweb.vehiclerouting.plugin.rest.model.ServerInfo;
import org.optaweb.vehiclerouting.service.demo.DemoService;
import org.optaweb.vehiclerouting.service.region.BoundingBox;
import org.optaweb.vehiclerouting.service.region.RegionService;
@Path("api/serverInfo")
public class ServerInfoResource {
private final DemoService demoService;
private final RegionService regionService;
@Inject
public ServerInfoResource(DemoService demoService, RegionService regionService) {
this.demoService = demoService;
this.regionService = regionService;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public ServerInfo serverInfo() {
BoundingBox boundingBox = regionService.boundingBox();
List portableBoundingBox = Arrays.asList(
PortableCoordinates.fromCoordinates(boundingBox.getSouthWest()),
PortableCoordinates.fromCoordinates(boundingBox.getNorthEast()));
List demos = demoService.demos().stream()
.map(routingProblem -> new RoutingProblemInfo(
routingProblem.name(),
routingProblem.visits().size()))
.collect(toList());
return new ServerInfo(portableBoundingBox, regionService.countryCodes(), demos);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/VehicleResource.java
================================================
package org.optaweb.vehiclerouting.plugin.rest;
import javax.inject.Inject;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.optaweb.vehiclerouting.service.vehicle.VehicleService;
@Path("api/vehicle")
public class VehicleResource {
private final VehicleService vehicleService;
@Inject
public VehicleResource(VehicleService vehicleService) {
this.vehicleService = vehicleService;
}
@POST
public void addVehicle() {
vehicleService.createVehicle();
}
/**
* Delete vehicle.
*
* @param id ID of the vehicle to be deleted
*/
@DELETE
@Path("{id}")
public void removeVehicle(@PathParam("id") long id) {
vehicleService.removeVehicle(id);
}
@POST
@Path("deleteAny")
public void removeAnyVehicle() {
vehicleService.removeAnyVehicle();
}
@POST
@Path("{id}/capacity")
public void changeCapacity(@PathParam("id") long id, int capacity) {
vehicleService.changeCapacity(id, capacity);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableCoordinates.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
import org.optaweb.vehiclerouting.domain.Coordinates;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* {@link Coordinates} representation optimized for network transport.
*/
public class PortableCoordinates {
/*
* Five decimal places gives "metric" precision (±55 cm on equator). That's enough for visualising the track.
* https://wiki.openstreetmap.org/wiki/Node#Structure
*/
private static final int LATLNG_SCALE = 5;
@JsonProperty(value = "lat")
private final BigDecimal latitude;
@JsonProperty(value = "lng")
private final BigDecimal longitude;
public static PortableCoordinates fromCoordinates(Coordinates coordinates) {
Objects.requireNonNull(coordinates, "coordinates must not be null");
return new PortableCoordinates(
coordinates.latitude(),
coordinates.longitude());
}
private static BigDecimal scale(BigDecimal number) {
return number.setScale(Math.min(number.scale(), LATLNG_SCALE), RoundingMode.HALF_EVEN).stripTrailingZeros();
}
PortableCoordinates(BigDecimal latitude, BigDecimal longitude) {
this.latitude = scale(latitude);
this.longitude = scale(longitude);
}
public BigDecimal getLatitude() {
return latitude;
}
public BigDecimal getLongitude() {
return longitude;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PortableCoordinates that = (PortableCoordinates) o;
return Objects.equals(latitude, that.latitude) &&
Objects.equals(longitude, that.longitude);
}
@Override
public int hashCode() {
return Objects.hash(latitude, longitude);
}
@Override
public String toString() {
return "PortableCoordinates{" +
"latitude=" + latitude +
", longitude=" + longitude +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableDistance.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.util.Objects;
import org.optaweb.vehiclerouting.domain.Distance;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Portable representation of a {@link Distance distance}.
*/
public class PortableDistance {
@JsonValue
private final String distance;
static PortableDistance fromDistance(Distance distance) {
long seconds = (Objects.requireNonNull(distance).millis() + 500) / 1000;
return new PortableDistance(String.format("%dh %dm %ds", seconds / 3600, seconds / 60 % 60, seconds % 60));
}
private PortableDistance(String distance) {
this.distance = distance;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PortableDistance that = (PortableDistance) o;
return distance.equals(that.distance);
}
@Override
public int hashCode() {
return Objects.hash(distance);
}
@Override
public String toString() {
return "PortableDistance{" +
"distance='" + distance + '\'' +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableErrorMessage.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.util.Objects;
import org.optaweb.vehiclerouting.service.error.ErrorMessage;
/**
* Portable error message.
*/
public class PortableErrorMessage {
private final String id;
private final String text;
public static PortableErrorMessage fromMessage(ErrorMessage message) {
return new PortableErrorMessage(message.id, message.text);
}
PortableErrorMessage(String id, String text) {
this.id = id;
this.text = text;
}
public String getId() {
return id;
}
public String getText() {
return text;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PortableErrorMessage that = (PortableErrorMessage) o;
return id.equals(that.id) &&
text.equals(that.text);
}
@Override
public int hashCode() {
return Objects.hash(id, text);
}
@Override
public String toString() {
return "PortableErrorMessage{" +
"id='" + id + '\'' +
", text='" + text + '\'' +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableLocation.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.math.BigDecimal;
import java.util.Objects;
import org.optaweb.vehiclerouting.domain.Location;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* {@link Location} representation convenient for marshalling.
*/
public class PortableLocation {
private final long id;
@JsonProperty(value = "lat", required = true)
private final BigDecimal latitude;
@JsonProperty(value = "lng", required = true)
private final BigDecimal longitude;
private final String description;
static PortableLocation fromLocation(Location location) {
Objects.requireNonNull(location, "location must not be null");
return new PortableLocation(
location.id(),
location.coordinates().latitude(),
location.coordinates().longitude(),
location.description());
}
@JsonCreator
public PortableLocation(
@JsonProperty(value = "id") long id,
@JsonProperty(value = "lat") BigDecimal latitude,
@JsonProperty(value = "lng") BigDecimal longitude,
@JsonProperty(value = "description") String description) {
this.id = id;
this.latitude = Objects.requireNonNull(latitude);
this.longitude = Objects.requireNonNull(longitude);
this.description = Objects.requireNonNull(description);
}
public long getId() {
return id;
}
public BigDecimal getLatitude() {
return latitude;
}
public BigDecimal getLongitude() {
return longitude;
}
public String getDescription() {
return description;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PortableLocation that = (PortableLocation) o;
return id == that.id &&
description.equals(that.description) &&
latitude.compareTo(that.latitude) == 0 &&
longitude.compareTo(that.longitude) == 0;
}
@Override
public int hashCode() {
return Objects.hash(id, description, latitude, longitude);
}
@Override
public String toString() {
return "PortableLocation{" +
"id=" + id +
", description='" + description + '\'' +
", latitude=" + latitude +
", longitude=" + longitude +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRoute.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.util.List;
import java.util.Objects;
import org.optaweb.vehiclerouting.domain.Route;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* Vehicle {@link Route route} representation convenient for marshalling.
*/
class PortableRoute {
private final PortableVehicle vehicle;
private final PortableLocation depot;
private final List visits;
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
private final List> track;
PortableRoute(
PortableVehicle vehicle,
PortableLocation depot,
List visits,
List> track) {
this.vehicle = Objects.requireNonNull(vehicle);
this.depot = Objects.requireNonNull(depot);
this.visits = Objects.requireNonNull(visits);
this.track = Objects.requireNonNull(track);
}
public PortableVehicle getVehicle() {
return vehicle;
}
public PortableLocation getDepot() {
return depot;
}
public List getVisits() {
return visits;
}
public List> getTrack() {
return track;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRoutingPlan.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.util.List;
import org.optaweb.vehiclerouting.domain.RoutingPlan;
/**
* {@link RoutingPlan} representation convenient for marshalling.
*/
public class PortableRoutingPlan {
private final PortableDistance distance;
private final List vehicles;
private final PortableLocation depot;
private final List visits;
private final List routes;
PortableRoutingPlan(
PortableDistance distance,
List vehicles,
PortableLocation depot,
List visits,
List routes) {
// TODO require non-null
this.distance = distance;
this.vehicles = vehicles;
this.depot = depot;
this.visits = visits;
this.routes = routes;
}
public PortableDistance getDistance() {
return distance;
}
public List getVehicles() {
return vehicles;
}
public PortableLocation getDepot() {
return depot;
}
public List getVisits() {
return visits;
}
public List getRoutes() {
return routes;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRoutingPlanFactory.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.List;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.domain.RoutingPlan;
import org.optaweb.vehiclerouting.domain.Vehicle;
/**
* Creates instances of {@link PortableRoutingPlan}.
*/
public class PortableRoutingPlanFactory {
private PortableRoutingPlanFactory() {
throw new AssertionError("Utility class");
}
public static PortableRoutingPlan fromRoutingPlan(RoutingPlan routingPlan) {
PortableDistance distance = PortableDistance.fromDistance(routingPlan.distance());
List vehicles = portableVehicles(routingPlan.vehicles());
PortableLocation depot = routingPlan.depot().map(PortableLocation::fromLocation).orElse(null);
List visits = portableVisits(routingPlan.visits());
List routes = routingPlan.routes().stream()
.map(routeWithTrack -> new PortableRoute(
PortableVehicle.fromVehicle(routeWithTrack.vehicle()),
depot,
portableVisits(routeWithTrack.visits()),
portableTrack(routeWithTrack.track())))
.collect(toList());
return new PortableRoutingPlan(distance, vehicles, depot, visits, routes);
}
private static List> portableTrack(List> track) {
ArrayList> portableTrack = new ArrayList<>();
for (List segment : track) {
List portableSegment = segment.stream()
.map(PortableCoordinates::fromCoordinates)
.collect(toList());
portableTrack.add(portableSegment);
}
return portableTrack;
}
private static List portableVisits(List visits) {
return visits.stream()
.map(PortableLocation::fromLocation)
.collect(toList());
}
private static List portableVehicles(List vehicles) {
return vehicles.stream()
.map(PortableVehicle::fromVehicle)
.collect(toList());
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableVehicle.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.util.Objects;
import org.optaweb.vehiclerouting.domain.Vehicle;
/**
* {@link Vehicle} representation suitable for network transport.
*/
public class PortableVehicle {
private final long id;
private final String name;
private final int capacity;
static PortableVehicle fromVehicle(Vehicle vehicle) {
Objects.requireNonNull(vehicle, "vehicle must not be null");
return new PortableVehicle(vehicle.id(), vehicle.name(), vehicle.capacity());
}
PortableVehicle(long id, String name, int capacity) {
this.id = id;
this.name = Objects.requireNonNull(name);
this.capacity = capacity;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public int getCapacity() {
return capacity;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PortableVehicle vehicle = (PortableVehicle) o;
return id == vehicle.id &&
capacity == vehicle.capacity &&
name.equals(vehicle.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, capacity);
}
@Override
public String toString() {
return "PortableVehicle{" +
"id=" + id +
", name='" + name + '\'' +
", capacity=" + capacity +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/RoutingProblemInfo.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.util.Objects;
import org.optaweb.vehiclerouting.domain.RoutingProblem;
/**
* Information about a {@link RoutingProblem routing problem instance}.
*/
public class RoutingProblemInfo {
private final String name;
private final int visits;
public RoutingProblemInfo(String name, int visits) {
this.name = Objects.requireNonNull(name);
this.visits = visits;
}
/**
* Routing problem instance name.
*
* @return name
*/
public String getName() {
return name;
}
/**
* Number of visits in the routing problem instance.
*
* @return number of visits
*/
public int getVisits() {
return visits;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/ServerInfo.java
================================================
package org.optaweb.vehiclerouting.plugin.rest.model;
import java.util.List;
/**
* Server info suitable for network transport.
*/
public class ServerInfo {
private final List boundingBox;
private final List countryCodes;
private final List demos;
public ServerInfo(List boundingBox, List countryCodes, List demos) {
this.boundingBox = boundingBox;
this.countryCodes = countryCodes;
this.demos = demos;
}
public List getBoundingBox() {
return boundingBox;
}
public List getCountryCodes() {
return countryCodes;
}
public List getDemos() {
return demos;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/AirDistanceRouter.java
================================================
package org.optaweb.vehiclerouting.plugin.routing;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.service.distance.DistanceCalculator;
import org.optaweb.vehiclerouting.service.region.BoundingBox;
import org.optaweb.vehiclerouting.service.region.Region;
import org.optaweb.vehiclerouting.service.route.Router;
import io.quarkus.arc.properties.IfBuildProperty;
@ApplicationScoped
@IfBuildProperty(name = "app.routing.engine", stringValue = "AIR")
public class AirDistanceRouter implements Router, DistanceCalculator, Region {
protected static final int TRAVEL_SPEED_KPH = 60;
// Approximate Metric Equivalents for Degrees. At the equator for longitude and for latitude anywhere,
// the following approximations are valid: 1° = 111 km (or 60 nautical miles) 0.1° = 11.1 km.
protected static final double KILOMETERS_PER_DEGREE = 111;
protected static final long MILLIS_IN_ONE_HOUR = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
@Override
public long travelTimeMillis(Coordinates from, Coordinates to) {
BigDecimal latDiff = to.latitude().subtract(from.latitude());
BigDecimal lngDiff = to.longitude().subtract(from.longitude());
double distanceKilometers = Math.sqrt(latDiff.pow(2).add(lngDiff.pow(2)).doubleValue()) * KILOMETERS_PER_DEGREE;
return (long) Math.floor(distanceKilometers / TRAVEL_SPEED_KPH * MILLIS_IN_ONE_HOUR);
}
@Override
public List getPath(Coordinates from, Coordinates to) {
return Arrays.asList(from, to);
}
@Override
public BoundingBox getBounds() {
return new BoundingBox(Coordinates.of(-90, -180), Coordinates.of(90, 180));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/Constants.java
================================================
package org.optaweb.vehiclerouting.plugin.routing;
public class Constants {
public static final String GRAPHHOPPER_PROFILE = "optaweb_car";
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/GraphHopperRouter.java
================================================
package org.optaweb.vehiclerouting.plugin.routing;
import static java.util.stream.Collectors.toList;
import java.util.List;
import java.util.stream.StreamSupport;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.service.distance.DistanceCalculator;
import org.optaweb.vehiclerouting.service.distance.RoutingException;
import org.optaweb.vehiclerouting.service.region.BoundingBox;
import org.optaweb.vehiclerouting.service.region.Region;
import org.optaweb.vehiclerouting.service.route.Router;
import com.graphhopper.GHRequest;
import com.graphhopper.GHResponse;
import com.graphhopper.GraphHopper;
import com.graphhopper.ResponsePath;
import com.graphhopper.util.PointList;
import com.graphhopper.util.shapes.BBox;
import io.quarkus.arc.properties.IfBuildProperty;
/**
* Provides geographical information needed for route optimization.
*/
@ApplicationScoped
@IfBuildProperty(name = "app.routing.engine", stringValue = "GRAPHHOPPER", enableIfMissing = true)
class GraphHopperRouter implements Router, DistanceCalculator, Region {
private final GraphHopper graphHopper;
@Inject
GraphHopperRouter(GraphHopper graphHopper) {
this.graphHopper = graphHopper;
}
@Override
public List getPath(Coordinates from, Coordinates to) {
PointList points = getBestRoute(from, to).getPoints();
return StreamSupport.stream(points.spliterator(), false)
.map(ghPoint3D -> Coordinates.of(ghPoint3D.lat, ghPoint3D.lon))
.collect(toList());
}
@Override
public long travelTimeMillis(Coordinates from, Coordinates to) {
return getBestRoute(from, to).getTime();
}
private ResponsePath getBestRoute(Coordinates from, Coordinates to) {
GHRequest request = new GHRequest(
from.latitude().doubleValue(),
from.longitude().doubleValue(),
to.latitude().doubleValue(),
to.longitude().doubleValue()).setProfile(Constants.GRAPHHOPPER_PROFILE);
GHResponse response = graphHopper.route(request);
// TODO return wrapper that can hold both the result and error explanation instead of throwing exception
if (response.hasErrors()) {
throw new RoutingException("No route from (" + from + ") to (" + to + ")", response.getErrors().get(0));
}
return response.getBest();
}
@Override
public BoundingBox getBounds() {
BBox bounds = graphHopper.getBaseGraph().getBounds();
return new BoundingBox(
Coordinates.of(bounds.minLat, bounds.minLon),
Coordinates.of(bounds.maxLat, bounds.maxLon));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/RoutingConfig.java
================================================
package org.optaweb.vehiclerouting.plugin.routing;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.stream.Stream;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.Profiles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.graphhopper.GraphHopper;
import com.graphhopper.config.CHProfile;
import com.graphhopper.config.Profile;
import io.quarkus.arc.DefaultBean;
import io.quarkus.arc.profile.UnlessBuildProfile;
import io.quarkus.arc.properties.IfBuildProperty;
/**
* Configuration bean that creates a GraphHopper instance and allows to configure the path to OSM file
* through environment.
*/
@Dependent
class RoutingConfig {
private static final Logger logger = LoggerFactory.getLogger(RoutingConfig.class);
private final Path osmDir;
private final Path osmFile;
private final Optional osmDownloadUrl;
private final Path graphHopperDir;
private final Path graphDir;
@Inject
RoutingConfig(RoutingProperties routingProperties) {
osmDir = Paths.get(routingProperties.osmDir()).toAbsolutePath();
osmFile = osmDir.resolve(routingProperties.osmFile()).toAbsolutePath();
osmDownloadUrl = routingProperties.osmDownloadUrl();
graphHopperDir = Paths.get(routingProperties.ghDir());
String regionName = routingProperties.osmFile().replaceFirst("\\.osm\\.pbf$", "");
graphDir = graphHopperDir.resolve(regionName).toAbsolutePath();
}
/**
* Avoids creating a real GraphHopper instance when running a @QuarkusTest.
*
* @return real GraphHopper
*/
@UnlessBuildProfile(Profiles.TEST)
@IfBuildProperty(name = "app.routing.engine", stringValue = "GRAPHHOPPER", enableIfMissing = true)
@Produces
@DefaultBean
GraphHopper graphHopper() {
GraphHopper graphHopper = new GraphHopper();
graphHopper.setGraphHopperLocation(graphDir.toString());
if (graphDirIsNotEmpty()) {
logger.info("Loading existing GraphHopper graph from: {}", graphDir);
} else {
if (Files.notExists(osmFile)) {
initDirs();
if (!osmDownloadUrl.isPresent() || osmDownloadUrl.get().trim().isEmpty()) {
throw new IllegalStateException(
"The osmFile (" + osmFile + ") does not exist"
+ " and no download URL was provided.\n"
+ "Download the OSM file from https://download.geofabrik.de/ first"
+ " or provide an OSM file URL"
+ " using the app.routing.osm-download-url property.");
}
downloadOsmFile(osmDownloadUrl.get(), osmFile);
}
logger.info("Importing OSM file: {}", osmFile);
graphHopper.setOSMFile(osmFile.toString());
}
/*
* Define a profile for each type of request that's going to be made at runtime. We're only going to ask for the fastest
* route for a car, so we only need one profile.
*
* Change the weighting to "shortest" (and delete the graph directory to re-import it) to optimize for shortest routes.
*
* Add a second profile with "shortest" weighting (and delete the graph directory) to be able to change travel cost
* optimization goal at runtime.
*/
graphHopper.setProfiles(new Profile(Constants.GRAPHHOPPER_PROFILE).setVehicle("car").setWeighting("fastest"));
/*
* Quick overview of routing modes:
*
* Flexible mode:
* - Dijkstra or A*
* - able to change requirements per request
* Speed mode:
* - "Contraction Hierarchies" algorithm (CH)
* - still Dijkstra but on a "shortcut graph"
* Hybrid mode
* - landmark algorithm
* - flexible and fast
*
* See https://www.graphhopper.com/blog/2017/08/14/flexible-routing-15-times-faster/.
*/
// Use CH for the only profile we have.
graphHopper.getCHPreparationHandler().setCHProfiles(new CHProfile(Constants.GRAPHHOPPER_PROFILE));
graphHopper.importOrLoad();
logger.info("GraphHopper graph loaded");
return graphHopper;
}
/**
* Decide whether the graph can be loaded.
*
* @return true if the graph directory exists and is not empty
*/
private boolean graphDirIsNotEmpty() {
if (Files.notExists(graphDir)) {
return false;
}
try (Stream graphDirFiles = Files.list(graphDir)) {
// Defensive programming. Check if the graph dir is empty. That happens if the import fails
// for example due to OutOfMemoryError.
return graphDirFiles.findAny().isPresent();
} catch (IOException e) {
throw new RoutingEngineException("Cannot read contents of the graph directory (" + graphDir + ")", e);
}
}
private void initDirs() {
try {
Files.createDirectories(osmDir);
Files.createDirectories(graphHopperDir);
} catch (IOException e) {
throw new RoutingEngineException("Can't create directory for storing OSM download", e);
}
}
static void downloadOsmFile(String urlString, Path osmFile) {
HttpURLConnection con;
URL url;
try {
url = new URL(urlString);
con = (HttpURLConnection) url.openConnection();
} catch (MalformedURLException e) {
throw new RoutingEngineException("The OSM file URL is malformed", e);
} catch (IOException e) {
throw new RoutingEngineException("The OSM file cannot be downloaded", e);
}
try {
con.setRequestMethod("GET");
} catch (ProtocolException e) {
throw new IllegalStateException("Can't set request method", e);
}
con.setConnectTimeout(10000);
con.setReadTimeout(10000);
logger.info("Downloading OSM file from {}", urlString);
try {
Files.copy(con.getInputStream(), osmFile);
} catch (IOException e) {
throw new RoutingEngineException("OSM file download failed", e);
}
logger.info("File saved to {}", osmFile);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/RoutingEngineException.java
================================================
package org.optaweb.vehiclerouting.plugin.routing;
public class RoutingEngineException extends RuntimeException {
RoutingEngineException(String message, Throwable cause) {
super(message, cause);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/RoutingProperties.java
================================================
package org.optaweb.vehiclerouting.plugin.routing;
import java.util.Optional;
import io.smallrye.config.ConfigMapping;
@ConfigMapping(prefix = "app.routing")
public interface RoutingProperties {
/**
* Directory to read OSM files from.
*/
String osmDir();
/**
* Directory where GraphHopper graphs are stored.
*/
String ghDir();
/**
* OpenStreetMap file name.
*/
String osmFile();
/**
* URL of an .osm.pbf file that will be downloaded in case the file doesn't exist on the file system.
*/
Optional osmDownloadUrl();
/**
* Routing engine providing distances and paths.
*/
RoutingEngine engine();
enum RoutingEngine {
AIR,
GRAPHHOPPER
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/package-info.java
================================================
/**
* Provides information based on geographical data, for example fastest and shortest distances between locations.
*/
package org.optaweb.vehiclerouting.plugin.routing;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/DemoProperties.java
================================================
package org.optaweb.vehiclerouting.service.demo;
import java.util.Optional;
import io.smallrye.config.ConfigMapping;
@ConfigMapping(prefix = "app.demo")
public interface DemoProperties {
/**
* Directory with demo data sets.
*/
Optional dataSetDir();
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/DemoService.java
================================================
package org.optaweb.vehiclerouting.service.demo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.domain.RoutingProblem;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.service.demo.dataset.DataSetMarshaller;
import org.optaweb.vehiclerouting.service.location.LocationRepository;
import org.optaweb.vehiclerouting.service.location.LocationService;
import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository;
import org.optaweb.vehiclerouting.service.vehicle.VehicleService;
/**
* Performs demo-related use cases.
*/
@ApplicationScoped
public class DemoService {
static final int MAX_TRIES = 10;
private final RoutingProblemList routingProblems;
private final LocationService locationService;
private final LocationRepository locationRepository;
private final VehicleService vehicleService;
private final VehicleRepository vehicleRepository;
private final DataSetMarshaller dataSetMarshaller;
@Inject
public DemoService(
RoutingProblemList routingProblems,
LocationService locationService,
LocationRepository locationRepository,
VehicleService vehicleService,
VehicleRepository vehicleRepository,
DataSetMarshaller dataSetMarshaller) {
this.routingProblems = routingProblems;
this.locationService = locationService;
this.locationRepository = locationRepository;
this.vehicleService = vehicleService;
this.vehicleRepository = vehicleRepository;
this.dataSetMarshaller = dataSetMarshaller;
}
public Collection demos() {
return routingProblems.all();
}
public void loadDemo(String name) {
RoutingProblem routingProblem = routingProblems.byName(name);
// Add depot
routingProblem.depot().ifPresent(depot -> addWithRetry(depot.coordinates(), depot.description()));
// TODO start randomizing only after using all available cities (=> reproducibility for small demos)
routingProblem.visits().forEach(visit -> addWithRetry(visit.coordinates(), visit.description()));
routingProblem.vehicles().forEach(vehicleService::createVehicle);
}
private void addWithRetry(Coordinates coordinates, String description) {
int tries = 0;
while (tries < MAX_TRIES && !locationService.createLocation(coordinates, description).isPresent()) {
tries++;
}
if (tries == MAX_TRIES) {
throw new RuntimeException(
"Impossible to create a new location near " + coordinates + " after " + tries + " attempts");
}
}
public String exportDataSet() {
// FIXME still relying on the fact that the first location in the repository is the depot
List visits = new ArrayList<>(locationRepository.locations());
Location depot = visits.isEmpty() ? null : visits.remove(0);
List vehicles = vehicleRepository.vehicles();
return dataSetMarshaller.marshal(new RoutingProblem(
"Custom Vehicle Routing instance", vehicles, depot, visits));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/RoutingProblemConfig.java
================================================
package org.optaweb.vehiclerouting.service.demo;
import static java.util.stream.Collectors.toList;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.RoutingProblem;
import org.optaweb.vehiclerouting.service.demo.dataset.DataSetMarshaller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Configuration bean that produces the list of available routing problem data sets.
*/
@Dependent
class RoutingProblemConfig {
private static final Logger logger = LoggerFactory.getLogger(RoutingProblemConfig.class);
private final DemoProperties demoProperties;
private final DataSetMarshaller dataSetMarshaller;
@Inject
RoutingProblemConfig(DemoProperties demoProperties, DataSetMarshaller dataSetMarshaller) {
this.demoProperties = demoProperties;
this.dataSetMarshaller = dataSetMarshaller;
}
@Produces
RoutingProblemList routingProblems() {
Stream allProblems = Stream.concat(classPathProblems(), dataSetDirProblems());
return new RoutingProblemList(allProblems);
}
private Stream classPathProblems() {
return Stream.of(belgiumReader()).map(dataSetMarshaller::unmarshal);
}
private Stream dataSetDirProblems() {
return dataSetDir().map(dir -> collectProblems(dir).stream()).orElse(Stream.empty());
}
private List collectProblems(Path dataSetDirPath) {
try (Stream dataSetPaths = Files.list(dataSetDirPath)) {
return dataSetPaths
.map(Path::toFile)
.filter(file -> file.getName().endsWith(".yaml") && file.exists() && file.canRead())
.map(file -> {
try {
return new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
} catch (FileNotFoundException e) {
logger.error("Problem with dataset file {}", file, e);
return null;
}
})
.filter(Objects::nonNull)
// TODO make unmarshalling exception checked, catch it and ignore broken files
.map(dataSetMarshaller::unmarshal)
// Returning the stream here has no point because the stream is always closed by the try-with-resources.
.collect(toList());
} catch (IOException e) {
throw new IllegalStateException("Cannot list directory " + dataSetDirPath, e);
}
}
private Optional dataSetDir() {
// TODO watch the dir (and make this a service that has local/data resource as a dependency -> is testable)
Optional dataSetDirProperty = demoProperties.dataSetDir();
if (!dataSetDirProperty.isPresent()) {
logger.info("Data set directory (app.demo.data-set-dir) is not set.");
return Optional.empty();
}
Path dataSetDirPath = Paths.get(dataSetDirProperty.get());
if (!isReadableDir(dataSetDirPath)) {
logger.warn(
"Data set directory '{}' doesn't exist or cannot be read. No external data sets will be loaded.",
dataSetDirPath.toAbsolutePath());
return Optional.empty();
}
return Optional.of(dataSetDirPath);
}
private static Reader belgiumReader() {
return new InputStreamReader(
DemoService.class.getResourceAsStream("belgium-cities.yaml"),
StandardCharsets.UTF_8);
}
private static boolean isReadableDir(Path path) {
File file = path.toFile();
return file.exists() && file.canRead() && file.isDirectory();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/RoutingProblemList.java
================================================
package org.optaweb.vehiclerouting.service.demo;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.optaweb.vehiclerouting.domain.RoutingProblem;
/**
* Utility class that holds a map of routing problem instances and allows to look them up by name.
*/
class RoutingProblemList {
private final Map routingProblems;
RoutingProblemList(Stream routingProblems) {
this.routingProblems = Objects.requireNonNull(routingProblems)
// TODO use file name as the key (that's more likely to be unique than data set name)
.collect(toMap(RoutingProblem::name, identity()));
}
Collection all() {
return routingProblems.values();
}
RoutingProblem byName(String name) {
RoutingProblem routingProblem = routingProblems.get(name);
if (routingProblem == null) {
throw new IllegalArgumentException("Data set with name '" + name + "' doesn't exist");
}
return routingProblem;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSet.java
================================================
package org.optaweb.vehiclerouting.service.demo.dataset;
import java.util.List;
/**
* Data set representation used for marshalling and unmarshalling.
*/
class DataSet {
private String name;
private List vehicles;
private DataSetLocation depot;
private List visits;
/**
* Data set name (a short description).
*
* @return data set name (may be {@code null})
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* Vehicles.
*
* @return vehicles (may be {@code null})
*/
public List getVehicles() {
return vehicles;
}
public void setVehicles(List vehicles) {
this.vehicles = vehicles;
}
/**
* The depot.
*
* @return the depot (may be {@code null})
*/
public DataSetLocation getDepot() {
return depot;
}
public void setDepot(DataSetLocation depot) {
this.depot = depot;
}
/**
* Visits.
*
* @return visits (may be {@code null})
*/
public List getVisits() {
return visits;
}
public void setVisits(List visits) {
this.visits = visits;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSetLocation.java
================================================
package org.optaweb.vehiclerouting.service.demo.dataset;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Data set location.
*/
class DataSetLocation {
private String label;
@JsonProperty(value = "lat")
private double latitude;
@JsonProperty(value = "lng")
private double longitude;
private DataSetLocation() {
// for unmarshalling
}
DataSetLocation(String label, double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
this.label = label;
}
/**
* Location label.
*
* @return label
*/
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
/**
* Latitude.
*
* @return latitude
*/
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
/**
* Longitude.
*
* @return longitude
*/
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
@Override
public String toString() {
return "DataSetLocation{" +
"label='" + label + '\'' +
", latitude=" + latitude +
", longitude=" + longitude +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSetMarshaller.java
================================================
package org.optaweb.vehiclerouting.service.demo.dataset;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
import java.io.Reader;
import java.util.Collections;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.LocationData;
import org.optaweb.vehiclerouting.domain.RoutingProblem;
import org.optaweb.vehiclerouting.domain.VehicleData;
import org.optaweb.vehiclerouting.domain.VehicleFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
/**
* Data set marshaller using the YAML format.
*/
@ApplicationScoped
public class DataSetMarshaller {
private final ObjectMapper mapper;
/**
* Create marshaller using the default object mapper, which is set up to use YAML format.
*/
DataSetMarshaller() {
mapper = new ObjectMapper(new YAMLFactory());
}
/**
* Constructor for testing purposes.
*
* @param mapper usually a mock object mapper
*/
DataSetMarshaller(ObjectMapper mapper) {
this.mapper = mapper;
}
/**
* Unmarshal routing problem from a reader.
*
* @param reader a reader
* @return routing problem
*/
public RoutingProblem unmarshal(Reader reader) {
// TODO throw a checked exception that will force the caller to handle the reading problem
// (e.g. a bad format) and report it to the user or log an error
return toDomain(unmarshalToDataSet(reader));
}
/**
* Marshal routing problem to string.
*
* @param routingProblem routing problem
* @return string containing the marshaled routing problem
*/
public String marshal(RoutingProblem routingProblem) {
return marshal(toDataSet(routingProblem));
}
DataSet unmarshalToDataSet(Reader reader) {
try {
return mapper.readValue(reader, DataSet.class);
} catch (IOException e) {
throw new IllegalStateException("Can't read demo data set", e);
}
}
String marshal(DataSet dataSet) {
try {
return mapper.writeValueAsString(dataSet);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to marshal data set (" + dataSet.getName() + ")", e);
}
}
static DataSet toDataSet(RoutingProblem routingProblem) {
DataSet dataSet = new DataSet();
dataSet.setName(routingProblem.name());
dataSet.setDepot(routingProblem.depot().map(DataSetMarshaller::toDataSet).orElse(null));
dataSet.setVehicles(routingProblem.vehicles().stream()
.map(DataSetMarshaller::toDataSet)
.collect(toList()));
dataSet.setVisits(routingProblem.visits().stream()
.map(DataSetMarshaller::toDataSet)
.collect(toList()));
return dataSet;
}
static DataSetLocation toDataSet(LocationData locationData) {
return new DataSetLocation(
locationData.description(),
locationData.coordinates().latitude().doubleValue(),
locationData.coordinates().longitude().doubleValue());
}
static DataSetVehicle toDataSet(VehicleData vehicleData) {
return new DataSetVehicle(vehicleData.name(), vehicleData.capacity());
}
static RoutingProblem toDomain(DataSet dataSet) {
return new RoutingProblem(
Optional.ofNullable(dataSet.getName()).orElse(""),
Optional.ofNullable(dataSet.getVehicles()).orElse(Collections.emptyList())
.stream()
.map(DataSetMarshaller::toDomain)
.collect(toList()),
Optional.ofNullable(dataSet.getDepot()).map(DataSetMarshaller::toDomain).orElse(null),
Optional.ofNullable(dataSet.getVisits()).orElse(Collections.emptyList())
.stream()
.map(DataSetMarshaller::toDomain)
.collect(toList()));
}
static LocationData toDomain(DataSetLocation dataSetLocation) {
return new LocationData(
Coordinates.of(dataSetLocation.getLatitude(), dataSetLocation.getLongitude()),
dataSetLocation.getLabel());
}
static VehicleData toDomain(DataSetVehicle dataSetVehicle) {
return VehicleFactory.vehicleData(dataSetVehicle.name, dataSetVehicle.capacity);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSetVehicle.java
================================================
package org.optaweb.vehiclerouting.service.demo.dataset;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Data set vehicle.
*/
public class DataSetVehicle {
@JsonProperty
final String name;
@JsonProperty
final int capacity;
@JsonCreator
public DataSetVehicle(@JsonProperty("name") String name, @JsonProperty("capacity") int capacity) {
this.name = name;
this.capacity = capacity;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/package-info.java
================================================
/**
* Data set marshalling and unmarshalling.
*/
package org.optaweb.vehiclerouting.service.demo.dataset;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/package-info.java
================================================
/**
* Demo data set loading and exporting.
*/
package org.optaweb.vehiclerouting.service.demo;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/DistanceCalculator.java
================================================
package org.optaweb.vehiclerouting.service.distance;
import org.optaweb.vehiclerouting.domain.Coordinates;
/**
* Calculates distances between coordinates.
*/
public interface DistanceCalculator {
/**
* Calculate travel time in milliseconds.
*
* @param from origin
* @param to destination
* @return travel time in milliseconds
* @throws RoutingException when the distance between given coordinates cannot be calculated
*/
long travelTimeMillis(Coordinates from, Coordinates to);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/DistanceMatrixImpl.java
================================================
package org.optaweb.vehiclerouting.service.distance;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.service.location.DistanceMatrix;
import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow;
@ApplicationScoped
class DistanceMatrixImpl implements DistanceMatrix {
private final DistanceCalculator distanceCalculator;
private final Map> matrix = new HashMap<>();
@Inject
DistanceMatrixImpl(DistanceCalculator distanceCalculator) {
this.distanceCalculator = distanceCalculator;
}
@Override
public DistanceMatrixRow addLocation(Location newLocation) {
Map distancesToOthers = updateMatrixLazily(newLocation);
return locationId -> distancesToOthers.computeIfAbsent(locationId, wrongId -> {
throw new IllegalArgumentException(
"Distance from " + newLocation
+ " to " + wrongId
+ " hasn't been recorded.\n"
+ "We only know distances to " + distancesToOthers.keySet());
});
}
private Map updateMatrixLazily(Location location) {
// Matrix == distance rows.
// We're adding a whole new row with distances from the new location to existing ones.
// We're also creating a new column by "appending" a new cell to each existing row.
// This new column contains distances from each existing location to the new one.
return matrix.computeIfAbsent(location, newLocation -> {
// The map must be thread-safe because:
// - we're updating it from the parallel stream below
// - it is accessed from solver thread!
Map distancesToOthers = new ConcurrentHashMap<>(); // the new row
// distance to self is 0
distancesToOthers.put(newLocation.id(), Distance.ZERO);
// For all entries (rows) in the matrix:
matrix.entrySet().stream().parallel().forEach(distanceRow -> {
// Entry key is the existing (other) location.
Location other = distanceRow.getKey();
// Entry value is the data (cells) in the row (distances from the entry key location to any other).
Map distancesFromOther = distanceRow.getValue();
// Add a new cell to the row with the distance from the entry key location to the new location
// (results in a new column at the end of the loop).
distancesFromOther.put(newLocation.id(), calculateDistance(other, newLocation));
// Add a cell to the new distance's row.
distancesToOthers.put(other.id(), calculateDistance(newLocation, other));
});
return distancesToOthers;
});
}
private Distance calculateDistance(Location from, Location to) {
return Distance.ofMillis(distanceCalculator.travelTimeMillis(from.coordinates(), to.coordinates()));
}
@Override
public Distance distance(Location from, Location to) {
if (!matrix.containsKey(from)) {
throw new IllegalArgumentException("Unknown 'from' location (" + from + ")");
}
Map distanceRow = matrix.get(from);
if (!distanceRow.containsKey(to.id())) {
throw new IllegalArgumentException("Unknown 'to' location (" + to + ")");
}
return distanceRow.get(to.id());
}
@Override
public void put(Location from, Location to, Distance distance) {
matrix.computeIfAbsent(from, location -> new ConcurrentHashMap<>()).put(to.id(), distance);
}
@Override
public void removeLocation(Location location) {
// Remove the distance matrix row (distances from the removed location to others).
matrix.remove(location);
// TODO also remove the "column" of the matrix (distances from others to the removed location) to avoid memory
// leak.
// But this probably requires making DistanceMatrixRow immutable (otherwise there's a risk of NPEs in solver)
// and update PlanningLocations' distance maps through problem fact changes.
}
@Override
public void clear() {
matrix.clear();
}
/**
* Number of rows in the matrix.
*
* @return number of rows
*/
public int dimension() {
return matrix.size();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/DistanceRepository.java
================================================
package org.optaweb.vehiclerouting.service.distance;
import java.util.Optional;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.domain.Location;
/**
* Stores distances between locations.
*/
public interface DistanceRepository {
void saveDistance(Location from, Location to, Distance distance);
Optional getDistance(Location from, Location to);
void deleteDistances(Location location);
void deleteAll();
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/RoutingException.java
================================================
package org.optaweb.vehiclerouting.service.distance;
public class RoutingException extends RuntimeException {
public RoutingException(String message, Throwable cause) {
super(message, cause);
}
public RoutingException(String message) {
super(message);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/package-info.java
================================================
/**
* Distance matrix calculation.
*/
package org.optaweb.vehiclerouting.service.distance;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorEvent.java
================================================
package org.optaweb.vehiclerouting.service.error;
import java.util.Objects;
public class ErrorEvent {
public final String message;
/**
* Create a new {@code ApplicationEvent}.
*
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
* @param message error message (never {@code null})
*/
public ErrorEvent(Object source, String message) {
this.message = Objects.requireNonNull(message);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorListener.java
================================================
package org.optaweb.vehiclerouting.service.error;
import java.util.UUID;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
/**
* Creates messages from error events and passes them to consumers.
*/
@ApplicationScoped
public class ErrorListener {
private final Event errorMessageEvent;
@Inject
public ErrorListener(Event errorMessageEvent) {
this.errorMessageEvent = errorMessageEvent;
}
public void onErrorEvent(@Observes ErrorEvent event) {
errorMessageEvent.fire(ErrorMessage.of(UUID.randomUUID().toString(), event.message));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorMessage.java
================================================
package org.optaweb.vehiclerouting.service.error;
import java.util.Objects;
public class ErrorMessage {
/**
* Message ID (never {@code null}).
*/
public final String id;
/**
* Message text (never {@code null}).
*/
public final String text;
public static ErrorMessage of(String id, String text) {
return new ErrorMessage(id, text);
}
private ErrorMessage(String id, String text) {
this.id = Objects.requireNonNull(id);
this.text = Objects.requireNonNull(text);
}
@Override
public String toString() {
return "ErrorMessage{" +
"id='" + id + '\'' +
", text='" + text + '\'' +
'}';
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorMessageConsumer.java
================================================
package org.optaweb.vehiclerouting.service.error;
/**
* Consumes error messages.
*/
public interface ErrorMessageConsumer {
/**
* Consume an error message.
*
* @param message error message
*/
void consumeMessage(ErrorMessage message);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/package-info.java
================================================
/**
* Handles error events. For example an uncaught exception can be turned into an error event that is sent to the client.
*/
package org.optaweb.vehiclerouting.service.error;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/DistanceMatrix.java
================================================
package org.optaweb.vehiclerouting.service.location;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.domain.Location;
/**
* Holds distances between every pair of locations.
*/
public interface DistanceMatrix {
DistanceMatrixRow addLocation(Location location);
void removeLocation(Location location);
void clear();
Distance distance(Location from, Location to);
void put(Location from, Location to, Distance distance);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/DistanceMatrixRow.java
================================================
package org.optaweb.vehiclerouting.service.location;
import org.optaweb.vehiclerouting.domain.Distance;
/**
* Contains {@link Distance distances} from the location associated with this row to other locations.
*/
public interface DistanceMatrixRow {
/**
* Distance from this row's location to the given location.
*
* @param locationId target location
* @return time it takes to travel to the given location
*/
Distance distanceTo(long locationId);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/LocationPlanner.java
================================================
package org.optaweb.vehiclerouting.service.location;
import org.optaweb.vehiclerouting.domain.Location;
/**
* Optimizes the routing plan in response to location-related changes in the routing problem.
*/
public interface LocationPlanner {
void addLocation(Location location, DistanceMatrixRow distanceMatrixRow);
void removeLocation(Location location);
void removeAllLocations();
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/LocationRepository.java
================================================
package org.optaweb.vehiclerouting.service.location;
import java.util.List;
import java.util.Optional;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
/**
* Defines repository operations on locations.
*/
public interface LocationRepository {
/**
* Create a location with a unique ID.
*
* @param coordinates location's coordinates
* @param description description of the location
* @return a new location
*/
Location createLocation(Coordinates coordinates, String description);
/**
* Get all locations.
*
* @return all locations
*/
List locations();
/**
* Remove a location with the given ID.
*
* @param id location ID
* @return the removed location
*/
Location removeLocation(long id);
/**
* Remove all locations from the repository.
*/
void removeAll();
/**
* Find a location by its ID.
*
* @param locationId location's ID
* @return an Optional containing location with the given ID or empty Optional if there is no location with such ID
*/
Optional find(long locationId);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/LocationService.java
================================================
package org.optaweb.vehiclerouting.service.location;
import static java.util.Comparator.comparingLong;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.transaction.Transactional;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.service.distance.DistanceRepository;
import org.optaweb.vehiclerouting.service.error.ErrorEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Performs location-related use cases.
*/
@ApplicationScoped
public class LocationService {
private static final Logger logger = LoggerFactory.getLogger(LocationService.class);
private final LocationRepository repository;
private final DistanceRepository distanceRepository;
private final LocationPlanner planner; // TODO move to RoutingPlanService (SRP)
private final DistanceMatrix distanceMatrix;
private final Event errorEvent;
@Inject
LocationService(
LocationRepository repository,
DistanceRepository distanceRepository,
LocationPlanner planner,
DistanceMatrix distanceMatrix,
Event errorEvent) {
this.repository = repository;
this.distanceRepository = distanceRepository;
this.planner = planner;
this.distanceMatrix = distanceMatrix;
this.errorEvent = errorEvent;
}
public synchronized void addLocation(Location location) {
Objects.requireNonNull(location);
DistanceMatrixRow distanceMatrixRow = distanceMatrix.addLocation(location);
planner.addLocation(location, distanceMatrixRow);
}
@Transactional
public synchronized Optional createLocation(Coordinates coordinates, String description) {
Objects.requireNonNull(coordinates);
Objects.requireNonNull(description);
// TODO if (router.isLocationAvailable(coordinates))
Location location = repository.createLocation(coordinates, description);
Optional distanceMatrixRow = addToMatrix(location);
if (distanceMatrixRow.isPresent()) {
planner.addLocation(location, distanceMatrixRow.get());
return Optional.of(location);
} else {
repository.removeLocation(location.id());
return Optional.empty();
}
}
private Optional addToMatrix(Location location) {
try {
DistanceMatrixRow distanceMatrixRow = distanceMatrix.addLocation(location);
repository.locations().stream()
.filter(existingLocation -> !existingLocation.equals(location))
.forEach(existingLocation -> {
distanceRepository.saveDistance(location, existingLocation,
distanceMatrixRow.distanceTo(existingLocation.id()));
distanceRepository.saveDistance(existingLocation, location,
distanceMatrix.distance(existingLocation, location));
});
return Optional.of(distanceMatrixRow);
} catch (Exception e) {
logger.error(
"Failed to calculate distances for location {}, it will be discarded",
location.fullDescription(), e);
errorEvent.fire(new ErrorEvent(
this,
"Failed to calculate distances for location " + location.fullDescription()
+ ", it will be discarded.\n" + e.toString()));
return Optional.empty();
}
}
@Transactional
public synchronized void removeLocation(long id) {
Optional optionalLocation = repository.find(id);
if (!optionalLocation.isPresent()) {
errorEvent.fire(new ErrorEvent(this, "Location [" + id + "] cannot be removed because it doesn't exist."));
return;
}
Location removedLocation = optionalLocation.get();
List locations = repository.locations();
if (locations.size() > 1) {
Location depot = locations.stream()
.min(comparingLong(Location::id))
.orElseThrow(() -> new IllegalStateException(
"Impossible. Locations have size (" + locations.size() + ") but the stream is empty."));
if (removedLocation.equals(depot)) {
errorEvent.fire(new ErrorEvent(this, "You can only remove depot if there are no visits."));
return;
}
}
planner.removeLocation(removedLocation);
repository.removeLocation(id);
distanceMatrix.removeLocation(removedLocation);
distanceRepository.deleteDistances(removedLocation);
}
@Transactional
public synchronized void removeAll() {
planner.removeAllLocations();
repository.removeAll();
distanceMatrix.clear();
distanceRepository.deleteAll();
}
public void populateDistanceMatrix() {
repository.locations()
.forEach(from -> repository.locations().stream()
.filter(to -> !to.equals(from))
.forEach(to -> distanceMatrix.put(from, to, distanceRepository.getDistance(from, to)
.orElseThrow(() -> new IllegalStateException("Distance from: [" + from + "] to: [" + to
+ "] is missing in the distance repository. This should not happen.")))));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/package-info.java
================================================
/**
* Use cases that involve {@link org.optaweb.vehiclerouting.domain.Location locations}.
*/
package org.optaweb.vehiclerouting.service.location;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/BoundingBox.java
================================================
package org.optaweb.vehiclerouting.service.region;
import java.util.Objects;
import org.optaweb.vehiclerouting.domain.Coordinates;
/**
* Bounding box.
*/
public class BoundingBox {
private final Coordinates southWest;
private final Coordinates northEast;
/**
* Create bounding box. The box must have non-zero dimensions and the corners must be south-west and north-east.
*
* @param southWest south-west corner (minimal latitude and longitude)
* @param northEast north-east corner (maximal latitude and longitude)
*/
public BoundingBox(Coordinates southWest, Coordinates northEast) {
this.southWest = Objects.requireNonNull(southWest);
this.northEast = Objects.requireNonNull(northEast);
if (southWest.latitude().compareTo(northEast.latitude()) >= 0) {
throw new IllegalArgumentException(
"South-west corner latitude ("
+ southWest.latitude()
+ "N) must be less than north-east corner latitude ("
+ northEast.latitude()
+ "N)");
}
if (southWest.longitude().compareTo(northEast.longitude()) >= 0) {
throw new IllegalArgumentException(
"South-west corner longitude ("
+ southWest.longitude()
+ "E) must be less than north-east corner longitude ("
+ northEast.longitude()
+ "E)");
}
}
/**
* South-west corner of the bounding box.
*
* @return south-west corner (minimal latitude and longitude)
*/
public Coordinates getSouthWest() {
return southWest;
}
/**
* North-east corner of the bounding box.
*
* @return north-east corner (maximal latitude and longitude)
*/
public Coordinates getNorthEast() {
return northEast;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/Region.java
================================================
package org.optaweb.vehiclerouting.service.region;
public interface Region {
BoundingBox getBounds();
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/RegionProperties.java
================================================
package org.optaweb.vehiclerouting.service.region;
import java.util.List;
import java.util.Optional;
import io.smallrye.config.ConfigMapping;
@ConfigMapping(prefix = "app.region")
public interface RegionProperties {
/**
* Get country codes specified for the loaded OSM file (working region).
* The codes are expected to be in the ISO 3166-1 alpha-2 format.
*
* @return list of country codes (never {@code null})
*/
Optional> countryCodes();
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/RegionService.java
================================================
package org.optaweb.vehiclerouting.service.region;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
/**
* Provides information about the working region.
*/
@ApplicationScoped
public class RegionService {
private final RegionProperties regionProperties;
private final Region region;
@Inject
RegionService(RegionProperties regionProperties, Region region) {
this.regionProperties = regionProperties;
this.region = region;
}
/**
* Country codes matching the working region.
*
* @return country codes (never {@code null})
*/
public List countryCodes() {
return regionProperties.countryCodes().orElse(List.of());
}
/**
* Bounding box of the working region.
*
* @return bounding box of the working region.
*/
public BoundingBox boundingBox() {
return region.getBounds();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/package-info.java
================================================
/**
* Provides information about the application's working region.
*/
package org.optaweb.vehiclerouting.service.region;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/reload/ReloadService.java
================================================
package org.optaweb.vehiclerouting.service.reload;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.service.location.LocationRepository;
import org.optaweb.vehiclerouting.service.location.LocationService;
import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository;
import org.optaweb.vehiclerouting.service.vehicle.VehicleService;
import io.quarkus.runtime.StartupEvent;
/**
* Reloads data from repositories when the application starts.
*/
@ApplicationScoped
public class ReloadService {
private final VehicleRepository vehicleRepository;
private final VehicleService vehicleService;
private final LocationRepository locationRepository;
private final LocationService locationService;
@Inject
ReloadService(
VehicleRepository vehicleRepository,
VehicleService vehicleService,
LocationRepository locationRepository,
LocationService locationService) {
this.vehicleRepository = vehicleRepository;
this.vehicleService = vehicleService;
this.locationRepository = locationRepository;
this.locationService = locationService;
}
public void reload(@Observes StartupEvent startupEvent) {
vehicleRepository.vehicles().forEach(vehicleService::addVehicle);
locationService.populateDistanceMatrix();
locationRepository.locations().forEach(locationService::addLocation);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/reload/package-info.java
================================================
/**
* Loads the application state from repositories when it starts.
*/
package org.optaweb.vehiclerouting.service.reload;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/RouteChangedEvent.java
================================================
package org.optaweb.vehiclerouting.service.route;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.optaweb.vehiclerouting.domain.Distance;
/**
* Event published when the routing plan has been updated either by discovering a better route or by a change
* in the problem specification (vehicles, visits).
*/
public class RouteChangedEvent {
private final Distance distance;
private final List vehicleIds;
private final Long depotId;
private final List visitIds;
private final Collection routes;
/**
* Create a new ApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
* @param distance total distance of all vehicle routes
* @param vehicleIds vehicle IDs
* @param depotId depot ID (may be {@code null} if there are no locations)
* @param visitIds IDs of visits
* @param routes vehicle routes
*/
public RouteChangedEvent(
Object source,
Distance distance,
List vehicleIds,
Long depotId,
List visitIds,
Collection routes) {
this.distance = Objects.requireNonNull(distance);
this.vehicleIds = Objects.requireNonNull(vehicleIds);
this.depotId = depotId; // may be null (no depot)
this.visitIds = Objects.requireNonNull(visitIds);
this.routes = Objects.requireNonNull(routes);
}
/**
* IDs of all vehicles.
*
* @return vehicle IDs
*/
public List vehicleIds() {
return vehicleIds;
}
/**
* Routes of all vehicles.
*
* @return vehicle routes
*/
public Collection routes() {
return routes;
}
/**
* Routing plan distance.
*
* @return distance (never {@code null})
*/
public Distance distance() {
return distance;
}
/**
* The depot ID.
*
* @return depot ID
*/
public Optional depotId() {
return Optional.ofNullable(depotId);
}
public List visitIds() {
return visitIds;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/RouteListener.java
================================================
package org.optaweb.vehiclerouting.service.route;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import org.optaweb.vehiclerouting.Profiles;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.domain.Route;
import org.optaweb.vehiclerouting.domain.RouteWithTrack;
import org.optaweb.vehiclerouting.domain.RoutingPlan;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.service.location.LocationRepository;
import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.quarkus.arc.profile.UnlessBuildProfile;
/**
* Handles route updates emitted by optimization plugin.
*/
@ApplicationScoped
@UnlessBuildProfile(Profiles.TEST)
public class RouteListener {
private static final Logger logger = LoggerFactory.getLogger(RouteListener.class);
private final Router router;
private final VehicleRepository vehicleRepository;
private final LocationRepository locationRepository;
private final Event routingPlanEvent;
// TODO maybe remove state from the service and get best route from a repository
private RoutingPlan bestRoutingPlan;
@Inject
RouteListener(
Router router,
VehicleRepository vehicleRepository,
LocationRepository locationRepository,
Event routingPlanEvent) {
this.router = router;
this.vehicleRepository = vehicleRepository;
this.locationRepository = locationRepository;
this.routingPlanEvent = routingPlanEvent;
bestRoutingPlan = RoutingPlan.empty();
}
// TODO maybe @ObservesAsync?
public void onApplicationEvent(@Observes RouteChangedEvent event) {
// TODO persist the best solution
Location depot = event.depotId().flatMap(locationRepository::find).orElse(null);
try {
// TODO Introduce problem revision (every modification increases revision number, event will only
// be published if revision numbers match) to avoid looking for missing/extra vehicles/visits.
// This will also make it possible to get rid of the try-catch approach.
Map vehicleMap = event.vehicleIds().stream()
.collect(toMap(vehicleId -> vehicleId, this::findVehicleById));
Map visitMap = event.visitIds().stream()
.collect(toMap(visitId -> visitId, this::findLocationById));
List routes = event.routes().stream()
// list of deep locations
.map(shallowRoute -> new Route(
vehicleMap.get(shallowRoute.vehicleId),
findLocationById(shallowRoute.depotId),
shallowRoute.visitIds.stream()
.map(visitMap::get)
.collect(toList())))
// add tracks
.map(route -> new RouteWithTrack(route, track(route.depot(), route.visits())))
.collect(toList());
bestRoutingPlan = new RoutingPlan(
event.distance(),
new ArrayList<>(vehicleMap.values()),
depot,
new ArrayList<>(visitMap.values()),
routes);
routingPlanEvent.fire(bestRoutingPlan);
} catch (IllegalStateException e) {
logger.warn("Discarding an outdated routing plan: {}", e.toString());
}
}
private Vehicle findVehicleById(long id) {
return vehicleRepository.find(id).orElseThrow(() -> new IllegalStateException(
"Vehicle {id=" + id + "} not found in the repository"));
}
private Location findLocationById(long id) {
return locationRepository.find(id).orElseThrow(() -> new IllegalStateException(
"Location {id=" + id + "} not found in the repository"));
}
private List> track(Location depot, List route) {
if (route.isEmpty()) {
return Collections.emptyList();
}
ArrayList itinerary = new ArrayList<>();
itinerary.add(depot);
itinerary.addAll(route);
itinerary.add(depot);
List> paths = new ArrayList<>();
for (int i = 0; i < itinerary.size() - 1; i++) {
Location fromLocation = itinerary.get(i);
Location toLocation = itinerary.get(i + 1);
List path = router.getPath(fromLocation.coordinates(), toLocation.coordinates());
paths.add(path);
}
return paths;
}
public RoutingPlan getBestRoutingPlan() {
return bestRoutingPlan;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/Router.java
================================================
package org.optaweb.vehiclerouting.service.route;
import java.util.List;
import org.optaweb.vehiclerouting.domain.Coordinates;
/**
* Provides paths between locations.
*/
public interface Router {
/**
* Get path between two locations.
*
* @param from starting location
* @param to destination
* @return list of coordinates describing the path between given locations.
*/
List getPath(Coordinates from, Coordinates to);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/ShallowRoute.java
================================================
package org.optaweb.vehiclerouting.service.route;
import static java.util.stream.Collectors.joining;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
// TODO maybe remove this once we fork planning domain from optaplanner-examples
// because then we can hold a reference to the original location
/**
* Lightweight route description consisting of vehicle and location IDs instead of entities.
* This makes it easier to quickly construct and share result of route optimization
* without converting planning domain objects to business domain objects.
* Specifically, some information may be lost when converting business domain objects to planning domain
* because it's not needed for optimization (e.g. location address)
* and so it's impossible to reconstruct the original business object without looking into the repository.
*/
public class ShallowRoute {
/**
* Vehicle ID.
*/
public final long vehicleId;
/**
* Depot ID.
*/
public final long depotId;
/**
* Visit IDs (immutable, never {@code null}).
*/
public final List visitIds;
/**
* Create shallow route.
*
* @param vehicleId vehicle ID
* @param depotId depot ID
* @param visitIds visit IDs
*/
public ShallowRoute(long vehicleId, long depotId, List visitIds) {
this.vehicleId = vehicleId;
this.depotId = depotId;
this.visitIds = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(visitIds)));
}
@Override
public String toString() {
String route = Stream.concat(Stream.of(depotId), visitIds.stream())
.map(Object::toString)
.collect(joining("->", "[", "]"));
return vehicleId + ": " + route;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/package-info.java
================================================
/**
* Handles {@link org.optaweb.vehiclerouting.domain.RoutingPlan route} updates.
*/
package org.optaweb.vehiclerouting.service.route;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/VehiclePlanner.java
================================================
package org.optaweb.vehiclerouting.service.vehicle;
import org.optaweb.vehiclerouting.domain.Vehicle;
/**
* Optimizes the routing plan in response to vehicle-related changes in the routing problem.
*/
public interface VehiclePlanner {
void addVehicle(Vehicle vehicle);
void removeVehicle(Vehicle vehicle);
void removeAllVehicles();
void changeCapacity(Vehicle vehicle);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/VehicleRepository.java
================================================
package org.optaweb.vehiclerouting.service.vehicle;
import java.util.List;
import java.util.Optional;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.domain.VehicleData;
/**
* Defines repository operations on vehicles.
*/
public interface VehicleRepository {
/**
* Create a vehicle with a unique ID.
*
* @param capacity vehicle's capacity
* @return a new vehicle
*/
Vehicle createVehicle(int capacity);
/**
* Create a vehicle from the given data.
*
* @param vehicleData vehicle data
* @return a new vehicle
*/
Vehicle createVehicle(VehicleData vehicleData);
/**
* Get all vehicles.
*
* @return all vehicles
*/
List vehicles();
/**
* Remove a vehicle with the given ID.
*
* @param id vehicle's ID
* @return the removed vehicle
*/
Vehicle removeVehicle(long id);
/**
* Remove all vehicles from the repository.
*/
void removeAll();
/**
* Find a vehicle by its ID.
*
* @param vehicleId vehicle's ID
* @return an Optional containing vehicle with the given ID or empty Optional if there is no vehicle with such ID
*/
Optional find(long vehicleId);
Vehicle changeCapacity(long vehicleId, int capacity);
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/VehicleService.java
================================================
package org.optaweb.vehiclerouting.service.vehicle;
import static java.util.Comparator.comparingLong;
import java.util.Objects;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.domain.VehicleData;
@ApplicationScoped
public class VehicleService {
static final int DEFAULT_VEHICLE_CAPACITY = 10;
private final VehiclePlanner planner;
private final VehicleRepository vehicleRepository;
@Inject
public VehicleService(VehiclePlanner planner, VehicleRepository vehicleRepository) {
this.planner = planner;
this.vehicleRepository = vehicleRepository;
}
@Transactional
public Vehicle createVehicle() {
Vehicle vehicle = vehicleRepository.createVehicle(DEFAULT_VEHICLE_CAPACITY);
addVehicle(vehicle);
return vehicle;
}
@Transactional
public Vehicle createVehicle(VehicleData vehicleData) {
Vehicle vehicle = vehicleRepository.createVehicle(vehicleData);
addVehicle(vehicle);
return vehicle;
}
public void addVehicle(Vehicle vehicle) {
planner.addVehicle(Objects.requireNonNull(vehicle));
}
@Transactional
public void removeVehicle(long vehicleId) {
Vehicle vehicle = vehicleRepository.removeVehicle(vehicleId);
planner.removeVehicle(vehicle);
}
public synchronized void removeAnyVehicle() {
Optional first = vehicleRepository.vehicles().stream().min(comparingLong(Vehicle::id));
first.map(Vehicle::id).ifPresent(this::removeVehicle);
}
@Transactional
public void removeAll() {
planner.removeAllVehicles();
vehicleRepository.removeAll();
}
@Transactional
public void changeCapacity(long vehicleId, int capacity) {
Vehicle updatedVehicle = vehicleRepository.changeCapacity(vehicleId, capacity);
planner.changeCapacity(updatedVehicle);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/package-info.java
================================================
/**
* Performs use cases that involve vehicles.
*/
package org.optaweb.vehiclerouting.service.vehicle;
================================================
FILE: optaweb-vehicle-routing-backend/src/main/resources/.gitignore
================================================
/application-local.properties
================================================
FILE: optaweb-vehicle-routing-backend/src/main/resources/application.properties
================================================
# App configuration
app.demo.data-set-dir=local/dataset
app.region.country-codes=BE
app.routing.osm-dir=local/openstreetmap
app.routing.gh-dir=local/graphhopper
app.routing.osm-file=belgium-latest.osm.pbf
app.routing.engine=GRAPHHOPPER
%test.app.routing.engine=GRAPHHOPPER
# OptaPlanner
quarkus.optaplanner.solver.daemon=true
quarkus.optaplanner.solver.termination.spent-limit=1m
# Enable CORS filter.
quarkus.http.cors=true
# Logging
quarkus.log.level=INFO
quarkus.log.min-level=DEBUG
quarkus.log.category."org.optaweb".level=${LOG_LEVEL_APP:INFO}
quarkus.log.category."org.optaplanner".level=${LOG_LEVEL_OPTAPLANNER:INFO}
quarkus.log.category."org.drools".level=${LOG_LEVEL_DROOLS:INFO}
quarkus.log.category."org.hibernate".level=${LOG_LEVEL_HIBERNATE:INFO}
quarkus.log.category."org.jboss.resteasy".level=${LOG_LEVEL_RESTEASY:INFO}
# In development mode, the working directory is ./target but local files are expected to survive mvn clean,
# so they should be one level above target.
%dev.app.demo.data-set-dir=../local/dataset
%dev.app.routing.osm-dir=../local/openstreetmap
%dev.app.routing.gh-dir=../local/graphhopper
############
# Datasource
############
# [PostgreSQL] - recommended for production
%postgresql.quarkus.datasource.db-kind=postgresql
%postgresql.quarkus.datasource.jdbc.url=jdbc:postgresql://${DATABASE_HOST:postgresql}:5432/${DATABASE_NAME:optaweb_vrp_database}
%postgresql.quarkus.datasource.username=${DATABASE_USER}
%postgresql.quarkus.datasource.password=${DATABASE_PASSWORD}
%postgresql.quarkus.hibernate-orm.database.generation=update
# [Default] - works out of the box, good for development
# - using an embedded DB with relative path: http://h2database.com/html/features.html#embedded_databases
# - not closing the DB automatically: http://h2database.com/html/features.html#closing_a_database
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:file:${app.persistence.h2-dir:../local/db}/${app.persistence.h2-filename:optaweb_vrp_database};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false
quarkus.datasource.username=sa
quarkus.datasource.password=
quarkus.hibernate-orm.database.generation=update
# [Tests]
# - using an in-memory DB: http://h2database.com/html/features.html#in_memory_databases
# - not closing the DB automatically: http://h2database.com/html/features.html#closing_a_database
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
%test.quarkus.datasource.username=sa
%test.quarkus.datasource.password=
%test.quarkus.hibernate-orm.database.generation=create-drop
%test.app.region.country-codes=AT,DE
###############
# Cypress tests
###############
%test-cypress.app.region.country-codes=DE
%test-cypress.app.routing.osm-dir=target-classes/src/test/resources/org/optaweb/vehiclerouting/plugin/routing
%test-cypress.app.routing.osm-file=planet_12.032,53.0171_12.1024,53.0491.osm.pbf
%test-cypress.optaplanner.solver.termination.spent-limit=10s
%test-cypress.quarkus.datasource.db-kind=h2
%test-cypress.quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
%test-cypress.quarkus.datasource.username=sa
%test-cypress.quarkus.datasource.password=
================================================
FILE: optaweb-vehicle-routing-backend/src/main/resources/org/optaweb/vehiclerouting/service/demo/belgium-cities.yaml
================================================
---
name: "Belgium cities"
vehicles:
- name: "Vehicle 1"
capacity: 10
- name: "Vehicle 2"
capacity: 10
- name: "Vehicle 3"
capacity: 10
- name: "Vehicle 4"
capacity: 10
- name: "Vehicle 5"
capacity: 10
- name: "Vehicle 6"
capacity: 10
depot:
label: "Brussels"
lat: 50.85
lng: 4.35
visits:
- label: "Aalst"
lat: 50.933333
lng: 4.033333
- label: "Anderlecht"
lat: 50.833333
lng: 4.333333
- label: "Antwerp"
lat: 51.217778
lng: 4.400278
- label: "Beringen"
lat: 51.033333
lng: 5.216667
- label: "Bruges"
lat: 51.216667
lng: 3.233333
- label: "Charleroi"
lat: 50.4
lng: 4.433333
- label: "Chatelet"
lat: 50.4
lng: 4.516667
- label: "Dendermonde"
lat: 51.033333
lng: 4.1
- label: "Geel"
lat: 51.166667
lng: 5.0
- label: "Genk"
lat: 50.966667
lng: 5.5
- label: "Ghent"
lat: 51.05
lng: 3.733333
- label: "Halle"
lat: 50.733333
lng: 4.233333
- label: "Hasselt"
lat: 50.93
lng: 5.34
- label: "Ixelles"
lat: 50.833333
lng: 4.366667
- label: "Kortrijk"
lat: 50.833333
lng: 3.266667
- label: "La Louviere"
lat: 50.466667
lng: 4.183333
- label: "Leuven"
lat: 50.883333
lng: 4.7
- label: "Liege"
lat: 50.633333
lng: 5.566667
- label: "Lier"
lat: 51.133333
lng: 4.566667
- label: "Lokeren"
lat: 51.1
lng: 3.983333
- label: "Mechelen"
lat: 51.016667
lng: 4.466667
- label: "Molenbeek"
lat: 50.857778
lng: 4.315833
- label: "Mons"
lat: 50.45
lng: 3.95
- label: "Namur"
lat: 50.466667
lng: 4.866667
- label: "Ninove"
lat: 50.833333
lng: 4.016667
- label: "Roeselare"
lat: 50.933333
lng: 3.116667
- label: "Schaerbeek"
lat: 50.866667
lng: 4.383333
- label: "Seraing"
lat: 50.583333
lng: 5.5
- label: "Sint Niklaas"
lat: 51.166667
lng: 4.133333
- label: "Sint Truiden"
lat: 50.8
lng: 5.183333
- label: "Tienen"
lat: 50.8
lng: 4.933333
- label: "Tournai"
lat: 50.6
lng: 3.383333
- label: "Turnhout"
lat: 51.316667
lng: 4.95
- label: "Uccle"
lat: 50.8
lng: 4.333333
- label: "Verviers"
lat: 50.583333
lng: 5.85
- label: "Vilvoorde"
lat: 50.933333
lng: 4.416667
- label: "Waregem"
lat: 50.883333
lng: 3.416667
- label: "Wavre"
lat: 50.716667
lng: 4.6
- label: "Ypres"
lat: 50.85
lng: 2.883333
================================================
FILE: optaweb-vehicle-routing-backend/src/main/resources/solverConfig.xml
================================================
org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution
org.optaweb.vehiclerouting.plugin.planner.domain.Standstill
org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit
org.optaweb.vehiclerouting.plugin.planner.VehicleRoutingConstraintProvider
ONLY_DOWN
FIRST_FIT_DECREASING
true
true
200
1
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/TestConfig.java
================================================
package org.optaweb.vehiclerouting;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import org.mockito.Mockito;
import org.optaweb.vehiclerouting.service.route.RouteListener;
import com.graphhopper.GraphHopper;
import io.quarkus.arc.profile.IfBuildProfile;
import io.quarkus.test.junit.QuarkusTest;
@Dependent
public class TestConfig {
/**
* Creates a GraphHopper mock that may be used when running a {@link QuarkusTest @QuarkusTest}.
*
* @return mock GraphHopper
*/
@IfBuildProfile(Profiles.TEST)
@Produces
public GraphHopper graphHopper() {
return Mockito.mock(GraphHopper.class);
}
/**
* Creates a mock route listener to avoid things like touching database and WebSocket.
*
* @return mock RouteListener
*/
@IfBuildProfile(Profiles.TEST)
@Produces
public RouteListener routeListener() {
return Mockito.mock(RouteListener.class);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/CoordinatesTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class CoordinatesTest {
@Test
void constructor_params_must_not_be_null() {
assertThatNullPointerException().isThrownBy(() -> new Coordinates(null, BigDecimal.ZERO));
assertThatNullPointerException().isThrownBy(() -> new Coordinates(BigDecimal.ZERO, null));
}
@Test
void coordinates_should_be_equals_when_numerically_equal() {
Coordinates coordinates = new Coordinates(BigDecimal.valueOf(987.1234), BigDecimal.valueOf(-0.1111));
assertThat(coordinates).isEqualTo(coordinates);
BigDecimal ONE_POINT_ZERO = new BigDecimal("1.0");
BigDecimal MINUS_ZERO = BigDecimal.ZERO.negate();
Coordinates coordinates01 = new Coordinates(BigDecimal.ZERO, BigDecimal.ONE);
assertThat(new Coordinates(MINUS_ZERO, ONE_POINT_ZERO))
.isEqualTo(coordinates01)
.hasSameHashCodeAs(coordinates01);
Coordinates coordinates10 = new Coordinates(BigDecimal.ONE, BigDecimal.ZERO);
assertThat(new Coordinates(ONE_POINT_ZERO, MINUS_ZERO))
.isEqualTo(coordinates10)
.hasSameHashCodeAs(coordinates10);
}
@Test
void should_not_equal() {
assertThat(new Coordinates(BigDecimal.ONE, BigDecimal.TEN))
.isNotEqualTo(null)
.isNotEqualTo(BigDecimal.valueOf(11))
.isNotEqualTo(new Coordinates(BigDecimal.ONE, BigDecimal.ONE))
.isNotEqualTo(new Coordinates(BigDecimal.TEN, BigDecimal.TEN));
}
@Test
void valueOf_and_getters() {
double latitude = Math.E;
double longitude = Math.PI;
Coordinates coordinates = Coordinates.of(latitude, longitude);
assertThat(coordinates.latitude()).isEqualTo(BigDecimal.valueOf(latitude));
assertThat(coordinates.longitude()).isEqualTo(BigDecimal.valueOf(longitude));
}
@Test
void toString_should_contain_latitude_and_longitude() {
String pi = "3.14159265358979323846";
assertThat(new Coordinates(BigDecimal.ONE, new BigDecimal(pi))).hasToString("[1, " + pi + "]");
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/CountryCodeValidatorTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.optaweb.vehiclerouting.domain.CountryCodeValidator.validate;
import java.util.ArrayList;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
class CountryCodeValidatorTest {
@Test
void should_fail_on_invalid_country_codes() {
assertThatNullPointerException().isThrownBy(() -> validate(null));
assertThatNullPointerException().isThrownBy(() -> validate(Arrays.asList("US", null, "CA")));
assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("XX")));
assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("CZE")));
assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("D")));
assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("")));
assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("US", "XY", "CA")));
}
@Test
void should_ignore_case_and_convert_to_upper_case() {
assertThat(validate(Arrays.asList("us"))).containsExactly("US");
}
@Test
void should_allow_multiple_values() {
assertThat(validate(Arrays.asList("US", "ca"))).containsExactly("US", "CA");
}
@Test
void should_allow_empty_list() {
assertThat(validate(new ArrayList<>())).isEmpty();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/DistanceTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import org.junit.jupiter.api.Test;
class DistanceTest {
@Test
void distance_millis_should_be_same_as_the_given_value() {
long millis = 123_999;
assertThat(Distance.ofMillis(millis).millis()).isEqualTo(millis);
}
@Test
void toString_should_contain_units_and_be_human_readable() {
assertThat(Distance.ofMillis(3600_000 * 37 + 60_000 * 3 + 24_000)).hasToString("37h 3m 24s 0ms");
assertThat(Distance.ofMillis(3601_000)).hasToString("1h 0m 1s 0ms");
assertThat(Distance.ofMillis(5_123)).hasToString("0h 0m 5s 123ms");
}
@Test
void time_must_be_positive_or_zero() {
assertThatIllegalArgumentException().isThrownBy(() -> Distance.ofMillis(-1)).withMessageContaining("(-1)");
assertThatCode(() -> Distance.ofMillis(0)).doesNotThrowAnyException();
}
@Test
void equals_hashCode() {
long millis = 37;
Distance distance = Distance.ofMillis(millis);
assertThat(distance)
.isEqualTo(distance)
.isEqualTo(Distance.ofMillis(millis))
.isNotEqualTo(null)
.isNotEqualTo(millis)
.isNotEqualTo(Distance.ofMillis(millis + 1))
.hasSameHashCodeAs(Distance.ofMillis(millis));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/LocationDataTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class LocationDataTest {
@Test
void constructor_params_must_not_be_null() {
assertThatNullPointerException().isThrownBy(() -> new LocationData(null, ""));
assertThatNullPointerException().isThrownBy(() -> new LocationData(Coordinates.of(1, 1), null));
}
@Test
void locations_are_equal_if_they_have_same_properties() {
Coordinates coordinates0 = new Coordinates(BigDecimal.ZERO, BigDecimal.ZERO);
Coordinates coordinates1 = new Coordinates(BigDecimal.ONE, BigDecimal.ONE);
String description = "test description";
LocationData equalLocationData = new LocationData(coordinates0, description);
final LocationData locationData = new LocationData(coordinates0, description);
assertThat(locationData)
// different coordinates
.isNotEqualTo(new LocationData(coordinates1, description))
// different description
.isNotEqualTo(new LocationData(coordinates0, "xyz"))
// null
.isNotEqualTo(null)
// different type with equal properties
.isNotEqualTo(new Location(0, coordinates0, description))
// same object -> OK
.isEqualTo(locationData)
// same properties -> OK
.isEqualTo(equalLocationData)
// equal objects => same hashCode
.hasSameHashCodeAs(equalLocationData);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/LocationTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class LocationTest {
@Test
void constructor_params_must_not_be_null() {
assertThatNullPointerException().isThrownBy(() -> new Location(0, null, ""));
assertThatNullPointerException().isThrownBy(() -> new Location(0, Coordinates.of(1, 1), null));
}
@Test
void locations_are_identified_based_on_id() {
final Coordinates coordinates0 = new Coordinates(BigDecimal.ZERO, BigDecimal.ZERO);
final Coordinates coordinates1 = new Coordinates(BigDecimal.ONE, BigDecimal.ONE);
final String description = "test description";
final long id = 0;
final Location location = new Location(id, coordinates0, description);
assertThat(location)
// different ID
.isNotEqualTo(new Location(1, coordinates0, description))
// null
.isNotEqualTo(null)
// different class
.isNotEqualTo(new LocationData(coordinates0, description))
// same object -> OK
.isEqualTo(location)
// same properties -> OK
.isEqualTo(new Location(id, coordinates0, description))
// same ID, different coordinate -> OK
.isEqualTo(new Location(id, coordinates1, description))
// same ID, different description -> OK
.isEqualTo(new Location(id, coordinates0, "xyz"));
}
@Test
void equal_locations_must_have_same_hashcode() {
long id = 1;
assertThat(new Location(id, Coordinates.of(1, 1), "description 1"))
.hasSameHashCodeAs(new Location(id, Coordinates.of(2, 2), "description 2"));
}
@Test
void constructor_without_description_should_create_empty_description() {
assertThat(new Location(7, Coordinates.of(3.14, 4.13)).description()).isEmpty();
}
@Test
void toString_should_contain_id_and_description() {
Coordinates coordinates = Coordinates.of(1, 1);
assertThat(new Location(7, coordinates, "")).hasToString("7");
assertThat(new Location(5, coordinates, "home")).hasToString("5: 'home'");
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/RouteTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
class RouteTest {
private final Vehicle vehicle = VehicleFactory.testVehicle(4);
private final Location depot = new Location(1, Coordinates.of(5, 5));
private final Location visit1 = new Location(2, Coordinates.of(5, 5));
private final Location visit2 = new Location(3, Coordinates.of(5, 5));
@Test
void constructor_args_not_null() {
assertThatNullPointerException().isThrownBy(() -> new Route(null, depot, Collections.emptyList()));
assertThatNullPointerException().isThrownBy(() -> new Route(vehicle, null, Collections.emptyList()));
assertThatNullPointerException().isThrownBy(() -> new Route(vehicle, depot, null));
}
@Test
void visits_should_not_contain_depot() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new Route(vehicle, depot, Arrays.asList(depot, visit1)))
.withMessageContaining(depot.toString());
assertThatIllegalArgumentException()
.isThrownBy(() -> new Route(vehicle, depot, Arrays.asList(visit1, depot, visit2)))
.withMessageContaining(depot.toString());
}
@Test
void no_visit_should_be_visited_twice_by_the_same_vehicle() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new Route(vehicle, depot, Arrays.asList(visit1, visit1)))
.withMessageContaining("(1)");
}
@Test
void cannot_modify_visits_externally() {
ArrayList visits = new ArrayList<>();
visits.add(visit1);
List routeVisits = new Route(vehicle, depot, visits).visits();
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(routeVisits::clear);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/RouteWithTrackTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
class RouteWithTrackTest {
private final Vehicle vehicle = VehicleFactory.testVehicle(4);
private final Location depot = new Location(1, Coordinates.of(5, 5));
private final Location visit1 = new Location(2, Coordinates.of(5, 5));
private final Location visit2 = new Location(3, Coordinates.of(5, 5));
@Test
void constructor_args_not_null() {
Route route = new Route(vehicle, depot, emptyList());
assertThatNullPointerException().isThrownBy(() -> new RouteWithTrack(route, null));
assertThatNullPointerException().isThrownBy(() -> new RouteWithTrack(null, emptyList()));
}
@Test
void cannot_modify_track_externally() {
Route route = new Route(vehicle, depot, Arrays.asList(visit1, visit2));
ArrayList> track = new ArrayList<>();
track.add(Arrays.asList(Coordinates.of(1.0, 2.0)));
List> routeTrack = new RouteWithTrack(route, track).track();
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(routeTrack::clear);
}
@Test
void when_route_is_empty_track_must_be_empty() {
Route emptyRoute = new Route(vehicle, depot, emptyList());
ArrayList> track = new ArrayList<>();
track.add(Arrays.asList(Coordinates.of(1.0, 2.0)));
assertThatIllegalArgumentException().isThrownBy(() -> new RouteWithTrack(emptyRoute, track));
}
@Test
void when_route_is_nonempty_track_must_be_nonempty() {
Route route = new Route(vehicle, depot, Arrays.asList(visit1, visit2));
ArrayList> emptyTrack = new ArrayList<>();
assertThatIllegalArgumentException().isThrownBy(() -> new RouteWithTrack(route, emptyTrack));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/RoutingPlanTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class RoutingPlanTest {
private final Distance distance = Distance.ofMillis(1);
private final Vehicle vehicle = VehicleFactory.testVehicle(1);
private final List vehicles = singletonList(vehicle);
private final Location depot = new Location(1, Coordinates.of(5, 5));
private final Location visit = new Location(2, Coordinates.of(3, 3));
private final RouteWithTrack emptyRoute = new RouteWithTrack(new Route(vehicle, depot, emptyList()), emptyList());
// Track is not important (we don't check if track starts and ends in the depot and goes through all visits)
private final List> nonEmptyTrack = singletonList(singletonList(Coordinates.of(5, 5)));
@Test
void constructor_args_not_null() {
assertThatNullPointerException()
.isThrownBy(() -> new RoutingPlan(null, vehicles, depot, emptyList(), emptyList()));
assertThatNullPointerException()
.isThrownBy(() -> new RoutingPlan(distance, null, depot, emptyList(), emptyList()));
assertThatNullPointerException()
.isThrownBy(() -> new RoutingPlan(distance, vehicles, depot, null, emptyList()));
assertThatNullPointerException()
.isThrownBy(() -> new RoutingPlan(distance, vehicles, depot, emptyList(), null));
// depot can be null
// TODO create a factory that will prevent passing a null depot accidentally
}
@Test
void no_visits_without_a_depot() {
List> track = singletonList(singletonList(visit.coordinates()));
RouteWithTrack routeWithTrack = new RouteWithTrack(new Route(vehicle, depot, singletonList(visit)), track);
assertThatIllegalArgumentException().isThrownBy(() -> new RoutingPlan(
distance, vehicles, null, singletonList(visit), singletonList(routeWithTrack)));
}
@Test
void no_routes_without_a_depot() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new RoutingPlan(distance, vehicles, null, emptyList(), singletonList(emptyRoute)));
}
@Test
void there_must_be_one_route_per_vehicle_when_there_is_a_depot() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new RoutingPlan(distance, vehicles, depot, emptyList(), emptyList()))
.withMessageContaining("Vehicles (1): [")
.withMessageContaining("Routes' vehicleIds (0): []");
}
@Test
void routes_vehicle_references_must_be_consistent_with_vehicles_in_routing_plan() {
List unexpectedVehicles = singletonList(VehicleFactory.testVehicle(vehicle.id() + 1));
List routes = singletonList(emptyRoute);
assertThatIllegalArgumentException()
.isThrownBy(() -> new RoutingPlan(distance, unexpectedVehicles, depot, emptyList(), routes))
.withMessageContaining("Vehicles (1): [")
.withMessageContaining("Routes' vehicleIds (1): [" + vehicle.id() + "]");
}
@Test
void routes_visit_references_must_be_consistent_with_visits_in_routing_plan() {
Vehicle vehicle1 = VehicleFactory.testVehicle(1);
Vehicle vehicle2 = VehicleFactory.testVehicle(2);
Location depot = new Location(100, Coordinates.of(0, 0), "depot");
Location visit1 = new Location(101, Coordinates.of(1, 1), "visit1");
Location visit2 = new Location(102, Coordinates.of(2, 2), "visit2");
Location visit3 = new Location(103, Coordinates.of(3, 3), "visit3");
assertThatCode(() -> new RoutingPlan(
distance,
emptyList(), // no vehicles
depot,
singletonList(visit1),
emptyList() // => no routes (no visits visited)
)).doesNotThrowAnyException();
assertThatIllegalArgumentException().isThrownBy(() -> new RoutingPlan(
distance,
singletonList(vehicle1),
depot,
asList(visit1, visit2),
singletonList(
// visit3 extra
new RouteWithTrack(new Route(vehicle1, depot, asList(visit1, visit2, visit3)), nonEmptyTrack))))
.withMessageContaining(visit3.toString());
Location visit4 = new Location(104, Coordinates.of(4, 4), "visit4");
assertThatIllegalArgumentException().isThrownBy(() -> new RoutingPlan(
distance,
asList(vehicle1, vehicle2),
depot,
asList(visit1, visit2, visit3),
// visit3 missing, visit4 extra
asList(
new RouteWithTrack(new Route(vehicle1, depot, asList(visit1, visit4)), nonEmptyTrack),
new RouteWithTrack(new Route(vehicle2, depot, singletonList(visit2)), nonEmptyTrack))))
.withMessageContaining(visit4.toString());
}
@Test
void cannot_modify_collections_externally() {
// Use modifiable collections as the input
ArrayList vehicles = new ArrayList<>();
ArrayList visits = new ArrayList<>();
ArrayList routes = new ArrayList<>();
vehicles.add(vehicle);
visits.add(visit);
routes.add(new RouteWithTrack(new Route(vehicle, depot, singletonList(visit)), nonEmptyTrack));
RoutingPlan routingPlan = new RoutingPlan(distance, vehicles, depot, visits, routes);
List planVehicles = routingPlan.vehicles();
List planVisits = routingPlan.visits();
List planRoutes = routingPlan.routes();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(planVehicles::clear);
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(planVisits::clear);
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(planRoutes::clear);
}
@Test
void empty_routing_plan_should_be_empty() {
RoutingPlan empty = RoutingPlan.empty();
assertThat(empty.distance()).isEqualTo(Distance.ZERO);
assertThat(empty.vehicles()).isEmpty();
assertThat(empty.depot()).isEmpty();
assertThat(empty.visits()).isEmpty();
assertThat(empty.routes()).isEmpty();
}
@Test
void isEmpty() {
assertThat(RoutingPlan.empty().isEmpty()).isTrue();
assertThat(new RoutingPlan(Distance.ZERO, emptyList(), depot, emptyList(), emptyList()).isEmpty()).isFalse();
assertThat(new RoutingPlan(Distance.ZERO, vehicles, null, emptyList(), emptyList()).isEmpty()).isFalse();
assertThat(new RoutingPlan(Distance.ZERO, vehicles, depot, emptyList(), singletonList(emptyRoute)).isEmpty())
.isFalse();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/VehicleDataTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import org.junit.jupiter.api.Test;
class VehicleDataTest {
@Test
void constructor_params_must_not_be_null() {
assertThatNullPointerException().isThrownBy(() -> new VehicleData(null, 1));
}
@Test
void vehicles_are_equal_if_they_have_same_properties() {
String name = "vehicle name";
int capacity = 20;
VehicleData vehicleData = new VehicleData(name, capacity);
assertThat(vehicleData)
// different name
.isNotEqualTo(new VehicleData("", capacity))
// different capacity
.isNotEqualTo(new VehicleData(name, capacity + 1))
// null
.isNotEqualTo(null)
// different type with equal properties
.isNotEqualTo(new Vehicle(0, name, capacity))
// same object -> OK
.isEqualTo(vehicleData)
// same properties -> OK
.isEqualTo(new VehicleData(name, capacity));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/VehicleFactoryTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class VehicleFactoryTest {
@Test
void createVehicle() {
long vehicleId = 4;
String name = "Vehicle four";
int capacity = 99;
Vehicle vehicle = VehicleFactory.createVehicle(vehicleId, name, capacity);
assertThat(vehicle.id()).isEqualTo(vehicleId);
assertThat(vehicle.name()).isEqualTo(name);
assertThat(vehicle.capacity()).isEqualTo(capacity);
}
@Test
void vehicleData() {
String name = "vehicle name";
int capacity = 1000;
VehicleData vehicleData = VehicleFactory.vehicleData(name, capacity);
assertThat(vehicleData.name()).isEqualTo(name);
assertThat(vehicleData.capacity()).isEqualTo(capacity);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/VehicleTest.java
================================================
package org.optaweb.vehiclerouting.domain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import org.junit.jupiter.api.Test;
class VehicleTest {
@Test
void constructor_params_must_not_be_null() {
assertThatNullPointerException().isThrownBy(() -> new Vehicle(0, null, 0));
}
@Test
void vehicles_are_identified_based_on_id() {
final long id = 0;
final String description = "test description";
final int capacity = 1;
final Vehicle vehicle = new Vehicle(id, description, capacity);
assertThat(vehicle)
// different ID
.isNotEqualTo(new Vehicle(id + 1, description, capacity))
// null
.isNotEqualTo(null)
// different class
.isNotEqualTo(id)
// same object -> OK
.isEqualTo(vehicle)
// same properties -> OK
.isEqualTo(new Vehicle(id, description, capacity))
// same ID, different description -> OK
.isEqualTo(new Vehicle(id, description + "x", capacity))
// same ID, different capacity -> OK
.isEqualTo(new Vehicle(id, description, capacity + 1));
}
@Test
void equal_vehicles_must_have_same_hashcode() {
long id = 1;
assertThat(new Vehicle(id, "description 1", 1))
.hasSameHashCodeAs(new Vehicle(id, "description 2", 2));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceEntityTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import org.junit.jupiter.api.Test;
class DistanceEntityTest {
@Test
void constructor_params_must_not_be_null() {
DistanceKey dKey = new DistanceKey(1, 2);
assertThatNullPointerException().isThrownBy(() -> new DistanceEntity(null, 100L));
assertThatNullPointerException().isThrownBy(() -> new DistanceEntity(dKey, null));
}
@Test
void equals() {
final long from = 10;
final long to = 2000;
final DistanceKey distanceKey = new DistanceKey(from, to);
final long distance = 50001;
DistanceEntity distanceEntity = new DistanceEntity(distanceKey, distance);
assertThat(distanceEntity)
.isEqualTo(distanceEntity)
.isEqualTo(new DistanceEntity(new DistanceKey(from, to), distance))
.isNotEqualTo(null)
.isNotEqualTo(distanceKey)
.isNotEqualTo(new DistanceEntity(distanceKey, distance + 1))
.isNotEqualTo(new DistanceEntity(new DistanceKey(to, from), distance));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceRepositoryImplTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.domain.Location;
@ExtendWith(MockitoExtension.class)
class DistanceRepositoryImplTest {
@Mock
private DistanceCrudRepository crudRepository;
@InjectMocks
private DistanceRepositoryImpl repository;
@Captor
private ArgumentCaptor distanceEntityArgumentCaptor;
private final Location from = new Location(1, Coordinates.of(7, -4.0));
private final Location to = new Location(2, Coordinates.of(5, 9.0));
@Test
void should_save_distance() {
long distance = 956766417;
repository.saveDistance(from, to, Distance.ofMillis(distance));
verify(crudRepository).persist(distanceEntityArgumentCaptor.capture());
DistanceEntity distanceEntity = distanceEntityArgumentCaptor.getValue();
assertThat(distanceEntity.getDistance()).isEqualTo(distance);
assertThat(distanceEntity.getKey().getFromId()).isEqualTo(from.id());
assertThat(distanceEntity.getKey().getToId()).isEqualTo(to.id());
}
@Test
void should_return_distance_when_entity_is_found() {
DistanceKey distanceKey = new DistanceKey(from.id(), to.id());
long distance = 10305;
DistanceEntity distanceEntity = new DistanceEntity(distanceKey, distance);
when(crudRepository.findByIdOptional(distanceKey)).thenReturn(Optional.of(distanceEntity));
assertThat(repository.getDistance(from, to)).contains(Distance.ofMillis(distance));
}
@Test
void should_return_negative_number_when_distance_not_found() {
when(crudRepository.findByIdOptional(any(DistanceKey.class))).thenReturn(Optional.empty());
assertThat(repository.getDistance(from, to)).isEmpty();
}
@Test
void should_delete_distance_by_location_id() {
repository.deleteDistances(from);
verify(crudRepository).deleteByFromIdOrToId(from.id());
}
@Test
void should_delete_all_distances() {
repository.deleteAll();
verify(crudRepository).deleteAll();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceRepositoryIntegrationTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import javax.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.domain.Location;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
class DistanceRepositoryIntegrationTest {
@Inject
DistanceCrudRepository crudRepository;
private DistanceRepositoryImpl repository;
@BeforeEach
void setUp() {
repository = new DistanceRepositoryImpl(crudRepository);
}
@Test
@TestTransaction
void panache_repository_should_persist_and_delete_distances() {
DistanceKey key = new DistanceKey(1, 2);
DistanceEntity entity = new DistanceEntity(key, 730107L);
crudRepository.persist(entity);
assertThat(crudRepository.count()).isOne();
assertThat(crudRepository.findById(key)).isEqualTo(entity);
crudRepository.deleteById(key);
assertThat(crudRepository.count()).isZero();
}
static DistanceEntity distance(long fromId, long toId) {
return new DistanceEntity(new DistanceKey(fromId, toId), 1L);
}
@Test
@TestTransaction
void delete_by_fromId_or_toId() {
DistanceEntity distance23 = distance(2, 3);
DistanceEntity distance32 = distance(3, 2);
crudRepository.persist(distance(1, 2));
crudRepository.persist(distance(2, 1));
crudRepository.persist(distance23);
crudRepository.persist(distance32);
crudRepository.persist(distance(3, 1));
crudRepository.persist(distance(1, 3));
assertThat(crudRepository.count()).isEqualTo(6);
crudRepository.deleteByFromIdOrToId(1L);
assertThat(crudRepository.count()).isEqualTo(2);
assertThat(crudRepository.findAll().stream()).containsExactly(distance23, distance32);
}
@Test
@TestTransaction
void should_return_saved_distance() {
Location location1 = new Location(1, Coordinates.of(7, -4.0));
Location location2 = new Location(2, Coordinates.of(5, 9.0));
Distance distance = Distance.ofMillis(956766417);
repository.saveDistance(location1, location2, distance);
assertThat(repository.getDistance(location1, location2)).contains(distance);
}
@Test
void should_return_negative_number_when_distance_not_found() {
Location location1 = new Location(1, Coordinates.of(7, -4.0));
Location location2 = new Location(2, Coordinates.of(5, 9.0));
assertThat(repository.getDistance(location1, location2)).isEmpty();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/LocationEntityTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class LocationEntityTest {
@Test
void constructor_params_must_not_be_null() {
assertThatNullPointerException().isThrownBy(() -> new LocationEntity(0, null, BigDecimal.ZERO, ""));
assertThatNullPointerException().isThrownBy(() -> new LocationEntity(0, BigDecimal.ZERO, null, ""));
assertThatNullPointerException().isThrownBy(() -> new LocationEntity(0, BigDecimal.ZERO, BigDecimal.ONE, null));
}
@Test
void getters() {
int id = 10;
BigDecimal latitude = BigDecimal.valueOf(0.101);
BigDecimal longitude = BigDecimal.valueOf(101.0);
String description = "Description.";
LocationEntity locationEntity = new LocationEntity(id, latitude, longitude, description);
assertThat(locationEntity.getId()).isEqualTo(id);
assertThat(locationEntity.getLongitude()).isEqualTo(longitude);
assertThat(locationEntity.getLatitude()).isEqualTo(latitude);
assertThat(locationEntity.getDescription()).isEqualTo(description);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/LocationRepositoryImplTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
@ExtendWith(MockitoExtension.class)
class LocationRepositoryImplTest {
@Mock
private LocationCrudRepository crudRepository;
@InjectMocks
private LocationRepositoryImpl repository;
@Captor
private ArgumentCaptor locationEntityCaptor;
private final Location testLocation = new Location(76, Coordinates.of(1.2, 3.4), "description");
private static LocationEntity locationEntity(Location location) {
return new LocationEntity(
location.id(),
location.coordinates().latitude(),
location.coordinates().longitude(),
location.description());
}
@Test
void should_create_location() {
// arrange
Coordinates savedCoordinates = Coordinates.of(0.00213, 32.777);
String savedDescription = "new location";
// act
Location newLocation = repository.createLocation(savedCoordinates, savedDescription);
// assert
// -- the correct values were used to save the entity
verify(crudRepository).persist(locationEntityCaptor.capture());
LocationEntity savedLocation = locationEntityCaptor.getValue();
assertThat(savedLocation.getLatitude()).isEqualTo(savedCoordinates.latitude());
assertThat(savedLocation.getLongitude()).isEqualTo(savedCoordinates.longitude());
assertThat(savedLocation.getDescription()).isEqualTo(savedDescription);
// -- created domain location has the expected values
assertThat(newLocation.coordinates()).isEqualTo(savedCoordinates);
assertThat(newLocation.description()).isEqualTo(savedDescription);
}
@Test
void remove_created_location_by_id() {
LocationEntity locationEntity = locationEntity(testLocation);
final long id = testLocation.id();
when(crudRepository.findByIdOptional(id)).thenReturn(Optional.of(locationEntity));
Location removed = repository.removeLocation(id);
assertThat(removed).isEqualTo(testLocation);
verify(crudRepository).deleteById(id);
}
@Test
void removing_nonexistent_location_should_fail() {
when(crudRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty());
// removing nonexistent location should fail and its ID should appear in the exception message
int uniqueNonexistentId = 7173;
assertThatIllegalArgumentException()
.isThrownBy(() -> repository.removeLocation(uniqueNonexistentId))
.withMessageContaining(String.valueOf(uniqueNonexistentId));
}
@Test
void remove_all_locations() {
repository.removeAll();
verify(crudRepository).deleteAll();
}
@Test
void get_all_locations() {
LocationEntity locationEntity = locationEntity(testLocation);
when(crudRepository.streamAll()).thenReturn(Stream.of(locationEntity));
assertThat(repository.locations()).containsExactly(testLocation);
}
@Test
void find_by_id() {
LocationEntity locationEntity = locationEntity(testLocation);
when(crudRepository.findByIdOptional(testLocation.id())).thenReturn(Optional.of(locationEntity));
assertThat(repository.find(testLocation.id())).contains(testLocation);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/LocationRepositoryIntegrationTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import java.math.BigDecimal;
import javax.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Location;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
class LocationRepositoryIntegrationTest {
@Inject
LocationCrudRepository crudRepository;
private LocationRepositoryImpl repository;
@BeforeEach
void setUp() {
repository = new LocationRepositoryImpl(crudRepository);
}
@Test
@TestTransaction
void db_schema() {
// https://wiki.openstreetmap.org/wiki/Node#Structure
final BigDecimal maxLatitude = new BigDecimal("90.0000000");
final BigDecimal maxLongitude = new BigDecimal("214.7483647");
final BigDecimal minLatitude = maxLatitude.negate();
final BigDecimal minLongitude = maxLongitude.negate();
final String description = "...";
LocationEntity minLocation = new LocationEntity(0, minLatitude, minLongitude, description);
LocationEntity maxLocation = new LocationEntity(0, maxLatitude, maxLongitude, description);
crudRepository.persist(minLocation);
crudRepository.persist(maxLocation);
assertThat(minLocation.getId()).isNotZero();
assertThat(maxLocation.getId()).isNotZero();
assertThat(crudRepository.findById(minLocation.getId())).isEqualTo(minLocation);
assertThat(crudRepository.findById(maxLocation.getId())).isEqualTo(maxLocation);
}
@Test
@TestTransaction
void remove_created_location() {
Coordinates coordinates = Coordinates.of(0.00213, 32.777);
assertThat(crudRepository.count()).isZero();
Location location = repository.createLocation(coordinates, "");
assertThat(location.coordinates()).isEqualTo(coordinates);
assertThat(crudRepository.count()).isOne();
Location removed = repository.removeLocation(location.id());
assertThat(removed).isEqualTo(location);
// removing the same location twice should fail
assertThatIllegalArgumentException().isThrownBy(() -> repository.removeLocation(location.id()));
// removing nonexistent location should fail and its ID should appear in the exception message
int uniqueNonexistentId = 7173;
assertThatIllegalArgumentException().isThrownBy(() -> repository.removeLocation(uniqueNonexistentId))
.withMessageContaining(String.valueOf(uniqueNonexistentId));
}
@Test
@TestTransaction
void get_and_remove_all_locations() {
int locationCount = 8;
for (int i = 0; i < locationCount; i++) {
repository.createLocation(Coordinates.of(1.0, i / 100.0), "");
}
assertThat(crudRepository.count()).isEqualTo(locationCount);
// get a sample location entity from the repository
LocationEntity testEntity = crudRepository
.findByIdOptional((long) locationCount)
.orElseThrow(IllegalStateException::new);
Location testLocation = new Location(
testEntity.getId(),
new Coordinates(testEntity.getLatitude(), testEntity.getLongitude()));
assertThat(repository.locations())
.hasSize(locationCount)
.contains(testLocation);
repository.removeAll();
assertThat(crudRepository.count()).isZero();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleEntityTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class VehicleEntityTest {
@Test
void getters() {
long id = 321;
String name = "Vehicle XY";
int capacity = 11;
VehicleEntity vehicleEntity = new VehicleEntity(id, name, capacity);
assertThat(vehicleEntity.getId()).isEqualTo(id);
assertThat(vehicleEntity.getName()).isEqualTo(name);
assertThat(vehicleEntity.getCapacity()).isEqualTo(capacity);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleRepositoryImplTest.java
================================================
package org.optaweb.vehiclerouting.plugin.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.domain.VehicleData;
import org.optaweb.vehiclerouting.domain.VehicleFactory;
@ExtendWith(MockitoExtension.class)
class VehicleRepositoryImplTest {
@Mock
private VehicleCrudRepository crudRepository;
@InjectMocks
private VehicleRepositoryImpl repository;
@Captor
private ArgumentCaptor vehicleEntityCaptor;
private final Vehicle testVehicle = VehicleFactory.createVehicle(19, "vehicle name", 1100);
private static VehicleEntity vehicleEntity(Vehicle vehicle) {
return new VehicleEntity(vehicle.id(), vehicle.name(), vehicle.capacity());
}
@Test
void should_create_vehicle() {
// arrange
int savedCapacity = 1;
// act
Vehicle newVehicle = repository.createVehicle(savedCapacity);
// assert
// -- the correct values were used to save the entity
verify(crudRepository).persist(vehicleEntityCaptor.capture());
VehicleEntity savedVehicle = vehicleEntityCaptor.getValue();
assertThat(savedVehicle.getName()).isNotNull();
assertThat(savedVehicle.getCapacity()).isEqualTo(savedCapacity);
// -- created domain vehicle has the expected values
assertThat(newVehicle.name()).isNotNull();
assertThat(newVehicle.capacity()).isEqualTo(savedCapacity);
}
@Test
void create_vehicle_from_given_data() {
// arrange
String name = "x";
int capacity = 111;
VehicleData vehicleData = VehicleFactory.vehicleData(name, capacity);
// act
Vehicle newVehicle = repository.createVehicle(vehicleData);
// assert
assertThat(newVehicle.name()).isEqualTo(name);
assertThat(newVehicle.capacity()).isEqualTo(capacity);
}
@Test
void remove_created_vehicle_by_id() {
VehicleEntity vehicleEntity = vehicleEntity(testVehicle);
final long id = testVehicle.id();
when(crudRepository.findByIdOptional(id)).thenReturn(Optional.of(vehicleEntity));
Vehicle removed = repository.removeVehicle(id);
assertThat(removed).isEqualTo(testVehicle);
verify(crudRepository).deleteById(id);
}
@Test
void removing_nonexistent_vehicle_should_fail() {
when(crudRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty());
// removing nonexistent vehicle should fail and its ID should appear in the exception message
int uniqueNonexistentId = 7173;
assertThatIllegalArgumentException()
.isThrownBy(() -> repository.removeVehicle(uniqueNonexistentId))
.withMessageContaining(String.valueOf(uniqueNonexistentId));
}
@Test
void remove_all_vehicles() {
repository.removeAll();
verify(crudRepository).deleteAll();
}
@Test
void get_all_vehicles() {
VehicleEntity vehicleEntity = vehicleEntity(testVehicle);
when(crudRepository.streamAll()).thenReturn(Stream.of(vehicleEntity));
assertThat(repository.vehicles()).containsExactly(testVehicle);
}
@Test
void find_by_id() {
VehicleEntity vehicleEntity = vehicleEntity(testVehicle);
when(crudRepository.findByIdOptional(testVehicle.id())).thenReturn(Optional.of(vehicleEntity));
assertThat(repository.find(testVehicle.id())).contains(testVehicle);
}
@Test
void update() {
long vehicleId = 123;
String name = "xy";
int capacity = 80;
VehicleEntity vehicleEntity = new VehicleEntity(vehicleId, name, capacity - 10);
when(crudRepository.findByIdOptional(vehicleId)).thenReturn(Optional.of(vehicleEntity));
Vehicle vehicle = repository.changeCapacity(vehicleId, capacity);
verify(crudRepository).flush();
assertThat(vehicleEntity.getCapacity()).isEqualTo(capacity);
assertThat(vehicle.id()).isEqualTo(vehicleId);
assertThat(vehicle.name()).isEqualTo(name);
assertThat(vehicle.capacity()).isEqualTo(capacity);
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/DistanceMapImplTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import org.junit.jupiter.api.Test;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory;
import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow;
class DistanceMapImplTest {
@Test
void matrix_row_must_not_be_null() {
assertThatNullPointerException().isThrownBy(() -> new DistanceMapImpl(null));
}
@Test
void distance_map_should_return_value_from_distance_matrix_row() {
PlanningLocation location2 = PlanningLocationFactory.testLocation(2);
Distance distance = Distance.ofMillis(45000);
DistanceMatrixRow matrixRow = locationId -> distance;
DistanceMapImpl distanceMap = new DistanceMapImpl(matrixRow);
assertThat(distanceMap.distanceTo(location2)).isEqualTo(distance.millis());
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/MockSolver.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.verify;
import org.mockito.Mockito;
import org.optaplanner.core.api.solver.change.ProblemChange;
import org.optaplanner.test.api.solver.change.MockProblemChangeDirector;
public class MockSolver {
private final Solution_ workingSolution;
private final MockProblemChangeDirector changeDirector;
public static MockSolver build(Solution_ solution) {
MockProblemChangeDirector spy = Mockito.spy(new MockProblemChangeDirector());
return new MockSolver<>(solution, spy);
}
private MockSolver(Solution_ workingSolution, MockProblemChangeDirector changeDirector) {
this.workingSolution = workingSolution;
this.changeDirector = changeDirector;
}
// ************************************************************************
// Problem change API from Solver.
// ************************************************************************
public void addProblemChange(ProblemChange problemChange) {
problemChange.doChange(workingSolution, changeDirector);
}
// ************************************************************************
// Lookup API from MockProblemChangeDirector.
// ************************************************************************
public MockProblemChangeDirector.LookUpMockBuilder whenLookingUp(Object forObject) {
return changeDirector.whenLookingUp(forObject);
}
// ************************************************************************
// Simplified verification API.
// ************************************************************************
public void verifyEntityAdded(Object entity) {
verify(changeDirector).addEntity(same(entity), any());
}
public void verifyEntityRemoved(Object entity) {
verify(changeDirector).removeEntity(same(entity), any());
}
public void verifyVariableChanged(Object entity, String variableName) {
verify(changeDirector).changeVariable(same(entity), eq(variableName), any());
}
public void verifyProblemFactAdded(Object fact) {
verify(changeDirector).addProblemFact(same(fact), any());
}
public void verifyProblemFactRemoved(Object fact) {
verify(changeDirector).removeProblemFact(same(fact), any());
}
public void verifyProblemPropertyChanged(Object entityOrFact) {
verify(changeDirector).changeProblemProperty(same(entityOrFact), any());
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/RouteChangedEventPublisherTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.testLocation;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory.testVehicle;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit;
import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.solutionFromVisits;
import javax.enterprise.event.Event;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import org.optaweb.vehiclerouting.service.route.RouteChangedEvent;
import org.optaweb.vehiclerouting.service.route.ShallowRoute;
@ExtendWith(MockitoExtension.class)
class RouteChangedEventPublisherTest {
@Mock
private Event publisher;
@InjectMocks
private RouteChangedEventPublisher routeChangedEventPublisher;
@Test
void should_covert_solution_to_event_and_publish_it() {
routeChangedEventPublisher.publishSolution(SolutionFactory.emptySolution());
verify(publisher).fire(any(RouteChangedEvent.class));
}
@Test
void empty_solution_should_have_zero_routes_vehicles_etc() {
VehicleRoutingSolution solution = SolutionFactory.emptySolution();
RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this);
assertThat(event.vehicleIds()).isEmpty();
assertThat(event.depotId()).isEmpty();
assertThat(event.visitIds()).isEmpty();
assertThat(event.routes()).isEmpty();
assertThat(event.distance()).isEqualTo(Distance.ZERO);
}
@Test
void solution_with_vehicles_and_no_depot_should_have_zero_routes() {
long vehicleId = 1;
PlanningVehicle vehicle = testVehicle(vehicleId);
VehicleRoutingSolution solution = solutionFromVisits(singletonList(vehicle), null, emptyList());
RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this);
assertThat(event.vehicleIds()).containsExactly(vehicleId);
assertThat(event.depotId()).isEmpty();
assertThat(event.visitIds()).isEmpty();
assertThat(event.routes()).isEmpty();
assertThat(event.distance()).isEqualTo(Distance.ZERO);
}
@Test
void nonempty_solution_without_vehicles_should_have_zero_routes_but_contain_visits() {
long depotId = 1;
long visitId = 2;
VehicleRoutingSolution solution = solutionFromVisits(
emptyList(),
new PlanningDepot(testLocation(depotId)),
singletonList(testVisit(visitId)));
RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this);
assertThat(event.vehicleIds()).isEmpty();
assertThat(event.depotId()).contains(depotId);
assertThat(event.visitIds()).containsExactly(visitId);
assertThat(event.routes()).isEmpty();
assertThat(event.distance()).isEqualTo(Distance.ZERO);
}
@Test
void initialized_solution_should_have_one_route_per_vehicle() {
// arrange
long vehicleId1 = 1001;
long vehicleId2 = 2001;
PlanningVehicle vehicle1 = testVehicle(vehicleId1);
PlanningVehicle vehicle2 = testVehicle(vehicleId2);
long depotId = 1;
long visitId1 = 2;
long visitId2 = 3;
PlanningDepot depot = new PlanningDepot(testLocation(depotId));
PlanningVisit visit1 = testVisit(visitId1);
PlanningVisit visit2 = testVisit(visitId2);
VehicleRoutingSolution solution = solutionFromVisits(
asList(vehicle1, vehicle2),
depot,
asList(visit1, visit2));
// Send both vehicles to both visits
// V1
// \
// |---> visit1 ---> visit2
// /
// V2
for (PlanningVehicle vehicle : solution.getVehicleList()) {
vehicle.setNextVisit(visit1);
visit1.setPreviousStandstill(vehicle);
}
visit1.setNextVisit(visit2);
visit2.setPreviousStandstill(visit1);
long softScore = -544564731;
solution.setScore(HardSoftLongScore.ofSoft(softScore));
// act
RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this);
// assert
assertThat(event.routes()).hasSameSizeAs(solution.getVehicleList());
assertThat(event.routes().stream().mapToLong(value -> value.vehicleId))
.containsExactlyInAnyOrder(vehicleId1, vehicleId2);
for (ShallowRoute route : event.routes()) {
assertThat(route.depotId).isEqualTo(depot.getId());
// visits shouldn't include the depot
assertThat(route.visitIds).containsExactly(visitId1, visitId2);
}
assertThat(event.vehicleIds()).containsExactlyInAnyOrder(vehicleId1, vehicleId2);
assertThat(event.depotId()).contains(depotId);
assertThat(event.visitIds()).containsExactlyInAnyOrder(visitId1, visitId2);
assertThat(event.distance()).isEqualTo(Distance.ofMillis(-softScore));
}
@Test
void fail_fast_if_vehicles_next_visit_doesnt_exist() {
PlanningVehicle vehicle = testVehicle(1);
vehicle.setNextVisit(testVisit(2));
VehicleRoutingSolution solution = solutionFromVisits(
singletonList(vehicle),
new PlanningDepot(testLocation(1)),
singletonList(testVisit(3)));
assertThatIllegalArgumentException()
.isThrownBy(() -> RouteChangedEventPublisher.solutionToEvent(solution, this))
.withMessageContaining("Visit");
}
@Test
void vehicle_without_a_depot_is_illegal_if_depot_exists() {
PlanningDepot depot = new PlanningDepot(testLocation(1));
PlanningVehicle vehicle = testVehicle(1);
VehicleRoutingSolution solution = solutionFromVisits(singletonList(vehicle), depot, emptyList());
vehicle.setDepot(null);
assertThatIllegalArgumentException()
.isThrownBy(() -> RouteChangedEventPublisher.solutionToEvent(solution, this))
.withMessageContaining("Vehicle");
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/RouteOptimizerImplTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.optaweb.vehiclerouting.domain.VehicleFactory.createVehicle;
import static org.optaweb.vehiclerouting.domain.VehicleFactory.testVehicle;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.fromDomain;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.optaweb.vehiclerouting.domain.Coordinates;
import org.optaweb.vehiclerouting.domain.Distance;
import org.optaweb.vehiclerouting.domain.Location;
import org.optaweb.vehiclerouting.domain.Vehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow;
@ExtendWith(MockitoExtension.class)
class RouteOptimizerImplTest {
private final DistanceMatrixRow matrixRow = locationId -> Distance.ZERO;
private final Location location1 = new Location(1, Coordinates.of(1.0, 0.1));
private final Location location2 = new Location(2, Coordinates.of(0.2, 2.2));
private final Location location3 = new Location(3, Coordinates.of(3.4, 5.6));
@Captor
private ArgumentCaptor solutionArgumentCaptor;
@Captor
private ArgumentCaptor vehicleArgumentCaptor;
@Mock
private SolverManager solverManager;
@Mock
private RouteChangedEventPublisher routeChangedEventPublisher;
@InjectMocks
private RouteOptimizerImpl routeOptimizer;
@Test
void solution_with_depot_and_no_visits_should_be_published() {
// arrange
Long[] vehicleIds = { 2L, 3L, 5L, 7L, 11L };
Arrays.stream(vehicleIds).forEach(vehicleId -> routeOptimizer.addVehicle(testVehicle(vehicleId)));
clearInvocations(routeChangedEventPublisher);
// act
routeOptimizer.addLocation(location1, matrixRow);
// assert
verifyNoInteractions(solverManager);
VehicleRoutingSolution solution = verifyPublishingPreliminarySolution();
assertThat(solution.getVehicleList())
.extracting(PlanningVehicle::getId)
.containsExactlyInAnyOrder(vehicleIds);
assertThat(solution.getDepotList()).extracting(PlanningDepot::getId).containsExactly(location1.id());
assertThat(solution.getVisitList()).isEmpty();
}
@Test
void solution_with_vehicles_and_no_depot_should_be_published() {
// arrange
final long vehicleId = 7;
final Vehicle vehicle = testVehicle(vehicleId);
// act 1
routeOptimizer.addVehicle(vehicle);
// assert 1
verifyNoInteractions(solverManager);
VehicleRoutingSolution solutionWithOneVehicle = verifyPublishingPreliminarySolution();
assertThat(solutionWithOneVehicle.getVehicleList())
.extracting(PlanningVehicle::getId)
.containsExactly(vehicleId);
assertThat(solutionWithOneVehicle.getDepotList()).isEmpty();
assertThat(solutionWithOneVehicle.getVisitList()).isEmpty();
// act 2
clearInvocations(routeChangedEventPublisher);
routeOptimizer.removeVehicle(vehicle);
// assert 2
verifyNoInteractions(solverManager);
VehicleRoutingSolution emptySolution = verifyPublishingPreliminarySolution();
assertThat(emptySolution.getVehicleList()).isEmpty();
assertThat(emptySolution.getDepotList()).isEmpty();
assertThat(emptySolution.getVisitList()).isEmpty();
}
@Test
void removing_wrong_vehicle_should_fail_fast() {
// arrange
final long vehicleId = 7;
final Vehicle vehicle = testVehicle(vehicleId);
final Vehicle nonExistentVehicle = testVehicle(vehicleId + 1);
routeOptimizer.addVehicle(vehicle);
// act & assert
assertThatIllegalArgumentException()
.isThrownBy(() -> routeOptimizer.removeVehicle(nonExistentVehicle))
.withMessageContaining("exist");
}
@Test
void removing_wrong_location_should_fail_fast() {
// no locations
assertThatIllegalArgumentException()
.isThrownBy(() -> routeOptimizer.removeLocation(location1))
.withMessageContaining("no locations");
// only depot
routeOptimizer.addLocation(location1, matrixRow);
assertThatIllegalArgumentException()
.isThrownBy(() -> routeOptimizer.removeLocation(location3))
.withMessageContaining("exist");
// depot and a visit
routeOptimizer.addLocation(location2, matrixRow);
assertThatIllegalArgumentException()
.isThrownBy(() -> routeOptimizer.removeLocation(location3))
.withMessageContaining("exist");
}
@Test
void added_vehicle_should_be_moved_to_the_depot_even_if_solver_is_not_yet_solving() {
// arrange
// -- depot
routeOptimizer.addLocation(location1, matrixRow);
// -- vehicles
routeOptimizer.addVehicle(testVehicle(7));
routeOptimizer.addVehicle(testVehicle(8));
// act
// -- first visit
routeOptimizer.addLocation(location2, matrixRow);
// assert
VehicleRoutingSolution solution = verifySolverStartedWithSolution();
assertThat(solution.getVehicleList())
.hasSize(2)
.allMatch(vehicle -> vehicle.getDepot().getId() == location1.id());
assertThat(solution.getDepotList()).isNotEmpty();
assertThat(solution.getVisitList()).isNotEmpty();
}
@Test
void solver_should_start_when_vehicle_is_added_and_there_is_at_least_one_visit() {
// arrange
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verifyNoInteractions(solverManager);
// act
routeOptimizer.addVehicle(testVehicle(9));
// assert
VehicleRoutingSolution solution = verifySolverStartedWithSolution();
assertThat(solution.getVehicleList()).hasSize(1);
assertThat(solution.getVisitList()).hasSize(1);
}
@Test
void each_location_should_have_a_distance_map_after_it_is_added() {
long millis = 8079;
routeOptimizer.addLocation(location1, locationId -> Distance.ofMillis(millis));
VehicleRoutingSolution solution = verifyPublishingPreliminarySolution();
assertThat(solution.getDepotList()).hasSize(1);
assertThat(solution.getDepotList().get(0).getLocation().distanceTo(fromDomain(location2))).isEqualTo(millis);
}
@Test
void solver_should_start_when_two_locations_added_and_there_is_at_least_one_vehicle() {
// add 1 vehicle, 2 locations
routeOptimizer.addVehicle(testVehicle(1));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
// solving has started after adding a second location (=> depot + visit)
VehicleRoutingSolution solution = verifySolverStartedWithSolution();
assertThat(solution.getDepotList()).hasSize(1);
assertThat(solution.getDepotList().get(0).getLocation().getId()).isEqualTo(location1.id());
assertThat(solution.getVisitList()).hasSize(1);
assertThat(solution.getVisitList().get(0).getLocation().getId()).isEqualTo(location2.id());
}
@Test
void solver_should_not_start_nor_stop_when_modifying_location_and_there_are_no_vehicles() {
// add 2 locations
routeOptimizer.addLocation(location1, matrixRow);
clearInvocations(routeChangedEventPublisher);
routeOptimizer.addLocation(location2, matrixRow);
// solving did not start due to missing vehicles
verify(solverManager, never()).startSolver(any());
// but preliminary solution is published
VehicleRoutingSolution solution1 = verifyPublishingPreliminarySolution();
assertThat(solution1.getVehicleList()).isEmpty();
assertThat(solution1.getDepotList()).hasSize(1);
assertThat(solution1.getVisitList()).hasSize(1);
// add a third location and remove another one
routeOptimizer.addLocation(location3, matrixRow);
clearInvocations(routeChangedEventPublisher);
routeOptimizer.removeLocation(location2);
// no interactions with solver (start/stop/problem fact changes) because
// it hasn't started (due to missing vehicles)
verifyNoInteractions(solverManager);
// but preliminary solution is published
VehicleRoutingSolution solution2 = verifyPublishingPreliminarySolution();
assertThat(solution2.getVehicleList()).isEmpty();
assertThat(solution1.getDepotList()).hasSize(1);
assertThat(solution1.getVisitList()).hasSize(1);
}
@Test
void solver_should_stop_and_publish_when_last_vehicle_is_removed() {
Vehicle vehicle = testVehicle(23);
routeOptimizer.addVehicle(vehicle);
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
clearInvocations(routeChangedEventPublisher);
routeOptimizer.removeVehicle(vehicle);
verify(solverManager).stopSolver();
VehicleRoutingSolution solution = verifyPublishingPreliminarySolution();
assertThat(solution.getVehicleList()).isEmpty();
}
@Test
void solver_should_stop_when_locations_reduced_to_one() {
// add 1 vehicle, 2 locations
routeOptimizer.addVehicle(testVehicle(0));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
clearInvocations(routeChangedEventPublisher);
// remove 1 location from running solver
routeOptimizer.removeLocation(location2);
verify(solverManager).stopSolver();
VehicleRoutingSolution solution = verifyPublishingPreliminarySolution();
assertThat(solution.getVisitList()).isEmpty();
assertThat(solution.getDepotList()).hasSize(1);
assertThat(solution.getVehicleList()).hasSize(1);
}
@Test
void removing_depot_impossible_when_there_are_other_locations() {
routeOptimizer.addVehicle(testVehicle(0));
// add 2 locations
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
assertThatIllegalStateException()
.isThrownBy(() -> routeOptimizer.removeLocation(location1))
.withMessageContaining("depot");
}
@Test
void when_depot_is_added_all_vehicles_should_be_moved_to_it() {
// given 2 vehicles
long vehicleId1 = 8;
long vehicleId2 = 113;
routeOptimizer.addVehicle(testVehicle(vehicleId1));
routeOptimizer.addVehicle(testVehicle(vehicleId2));
clearInvocations(routeChangedEventPublisher);
// when a depot is added
routeOptimizer.addLocation(location1, matrixRow);
// then all vehicles must be in the depot
VehicleRoutingSolution solution1 = verifyPublishingPreliminarySolution();
assertThat(solution1.getVehicleList())
.extracting(PlanningVehicle::getId)
.containsExactlyInAnyOrder(vehicleId1, vehicleId2);
assertThat(solution1.getVehicleList()).allMatch(vehicle -> vehicle.getDepot().getId() == location1.id());
assertThat(solution1.getDepotList()).extracting(PlanningDepot::getId).containsExactly(location1.id());
// if we remove the depot
clearInvocations(routeChangedEventPublisher);
routeOptimizer.removeLocation(location1);
// then published solution's depot list is empty
VehicleRoutingSolution solution2 = verifyPublishingPreliminarySolution();
assertThat(solution2.getVehicleList())
.extracting(PlanningVehicle::getId)
.containsExactlyInAnyOrder(vehicleId1, vehicleId2);
assertThat(solution2.getDepotList()).isEmpty();
// and it's possible to add a new depot
routeOptimizer.addLocation(location2, matrixRow);
}
@Test
void adding_location_to_running_solver_must_happen_through_problem_fact_change() {
// arrange
routeOptimizer.addVehicle(testVehicle(55));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
// act
routeOptimizer.addLocation(location3, matrixRow);
// assert
verify(solverManager).addVisit(any(PlanningVisit.class));
}
@Test
void removing_location_from_solver_with_more_than_two_locations_must_happen_through_problem_fact_change() {
// arrange: set up a situation where solver is running with 1 depot and 2 visits
long vehicleId = 0;
routeOptimizer.addVehicle(testVehicle(vehicleId));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
// add second visit to avoid stopping solver manager after removing a visit below
routeOptimizer.addLocation(location3, matrixRow);
verify(solverManager).addVisit(any(PlanningVisit.class));
// act
routeOptimizer.removeLocation(location2);
// assert
ArgumentCaptor visitArgumentCaptor = ArgumentCaptor.forClass(PlanningVisit.class);
verify(solverManager).removeVisit(visitArgumentCaptor.capture());
assertThat(visitArgumentCaptor.getValue().getId()).isEqualTo(location2.id());
// solver still running
verify(solverManager, never()).stopSolver();
}
@Test
void adding_vehicle_to_running_solver_must_happen_through_problem_fact_change() {
// arrange
routeOptimizer.addVehicle(testVehicle(1));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
// act
routeOptimizer.addVehicle(testVehicle(22));
// assert
verify(solverManager).addVehicle(vehicleArgumentCaptor.capture());
PlanningVehicle vehicle = vehicleArgumentCaptor.getValue();
assertThat(vehicle.getDepot().getId()).isEqualTo(location1.id());
}
@Test
void removing_vehicle_from_running_solver_with_more_than_one_vehicle_must_happen_through_problem_fact_change() {
// arrange: set up a situation where solver is running with 2 vehicles
final long vehicleId1 = 10;
final long vehicleId2 = 20;
routeOptimizer.addVehicle(testVehicle(vehicleId1));
routeOptimizer.addVehicle(testVehicle(vehicleId2));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
// act
routeOptimizer.removeVehicle(testVehicle(vehicleId1));
// assert
verify(solverManager).removeVehicle(any(PlanningVehicle.class));
// solver still running
verify(solverManager, never()).stopSolver();
}
@Test
void changing_vehicle_capacity_should_take_effect_when_solver_is_started_or_be_published() {
// 1 depot, 1 vehicle
final long vehicleId = 1;
final int oldCapacity = 7;
final int newCapacity = 12;
Vehicle vehicle = createVehicle(vehicleId, "", oldCapacity);
routeOptimizer.addVehicle(vehicle);
routeOptimizer.addLocation(location1, matrixRow);
clearInvocations(routeChangedEventPublisher);
// change capacity when solver is not running
routeOptimizer.changeCapacity(createVehicle(vehicleId, "", newCapacity));
verifyNoInteractions(solverManager);
VehicleRoutingSolution preliminarySolution = verifyPublishingPreliminarySolution();
assertThat(preliminarySolution.getVehicleList().get(0).getCapacity()).isEqualTo(newCapacity);
// start solver
routeOptimizer.addLocation(location2, matrixRow);
VehicleRoutingSolution solution = verifySolverStartedWithSolution();
assertThat(solution.getVehicleList().get(0).getCapacity()).isEqualTo(newCapacity);
}
@Test
void changing_vehicle_capacity_must_happen_through_problem_fact_change_when_solver_is_running() {
// 1 vehicle, 1 depot, 1 visit
final int capacity = 14816;
final long vehicleId = 10;
routeOptimizer.addVehicle(testVehicle(vehicleId));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
routeOptimizer.changeCapacity(createVehicle(vehicleId, "", capacity));
verify(solverManager).changeCapacity(any(PlanningVehicle.class));
}
@Test
void changing_vehicle_capacity_must_fail_fast_if_the_vehicle_does_not_exist() {
// 1 vehicle, 1 depot, 1 visit
final long vehicleId = 10;
routeOptimizer.addVehicle(testVehicle(vehicleId));
assertThatIllegalArgumentException()
.isThrownBy(() -> routeOptimizer.changeCapacity(testVehicle(vehicleId + 1)))
.withMessageContaining("exist");
}
@Test
void remove_all_locations_should_stop_solver_and_publish_preliminary_solution() {
// set up a situation where solver is running with 1 depot and 2 visits
long vehicleId = 10;
routeOptimizer.addVehicle(testVehicle(vehicleId));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
routeOptimizer.addLocation(location3, matrixRow);
clearInvocations(routeChangedEventPublisher);
routeOptimizer.removeAllLocations();
verify(solverManager).stopSolver();
VehicleRoutingSolution solution = verifyPublishingPreliminarySolution();
assertThat(solution.getVehicleList()).hasSize(1);
assertThat(solution.getDepotList()).isEmpty();
assertThat(solution.getVisitList()).isEmpty();
}
@Test
void remove_all_vehicles_should_stop_solver_and_publish_preliminary_solution() {
long vehicleId = 10;
routeOptimizer.addVehicle(testVehicle(vehicleId));
routeOptimizer.addLocation(location1, matrixRow);
routeOptimizer.addLocation(location2, matrixRow);
verify(solverManager).startSolver(any(VehicleRoutingSolution.class));
routeOptimizer.addLocation(location3, matrixRow);
clearInvocations(routeChangedEventPublisher);
routeOptimizer.removeAllVehicles();
verify(solverManager).stopSolver();
VehicleRoutingSolution solution = verifyPublishingPreliminarySolution();
assertThat(solution.getVehicleList()).isEmpty();
assertThat(solution.getDepotList()).hasSize(1);
assertThat(solution.getVisitList()).hasSize(2);
}
@Test
void removing_all_locations_should_not_fail_when_solver_is_not_solving() {
assertThatCode(() -> routeOptimizer.removeAllLocations()).doesNotThrowAnyException();
}
@Test
void removing_all_vehicles_should_not_fail_when_solver_is_not_solving() {
assertThatCode(() -> routeOptimizer.removeAllVehicles()).doesNotThrowAnyException();
}
private VehicleRoutingSolution verifyPublishingPreliminarySolution() {
verify(routeChangedEventPublisher).publishSolution(solutionArgumentCaptor.capture());
return solutionArgumentCaptor.getValue();
}
private VehicleRoutingSolution verifySolverStartedWithSolution() {
verify(solverManager).startSolver(solutionArgumentCaptor.capture());
return solutionArgumentCaptor.getValue();
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverExceptionTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import javax.enterprise.event.Event;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.optaplanner.core.api.solver.Solver;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import org.optaweb.vehiclerouting.service.error.ErrorEvent;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.common.util.concurrent.ListeningExecutorService;
@ExtendWith(MockitoExtension.class)
class SolverExceptionTest {
@Mock
private Solver solver;
@Mock
private ListeningExecutorService executor;
@Mock
private Event eventPublisher;
@Captor
ArgumentCaptor errorEventArgumentCaptor;
@InjectMocks
private SolverManager solverManager;
@Test
void should_publish_error_if_solver_stops_solving_without_being_terminated() {
// arrange
// Prepare a future that will be returned by mock executor
ListenableFutureTask task = ListenableFutureTask.create(SolutionFactory::emptySolution);
when(executor.submit(any(SolverManager.SolvingTask.class))).thenReturn(task);
// Run it synchronously (otherwise the test would be unreliable!)
task.run();
// act
solverManager.startSolver(SolutionFactory.emptySolution());
// assert
verify(eventPublisher).fire(errorEventArgumentCaptor.capture());
assertThat(errorEventArgumentCaptor.getValue().message).contains("This is a bug.");
}
@Test
void should_not_publish_error_if_solver_is_terminated_early() {
// arrange
// Prepare a future that will be returned by mock executor
ListenableFutureTask task = ListenableFutureTask.create(SolutionFactory::emptySolution);
when(executor.submit(any(SolverManager.SolvingTask.class))).thenReturn(task);
// Pretend the solver has been terminated by stopSolver()...
when(solver.isTerminateEarly()).thenReturn(true);
// act
solverManager.startSolver(SolutionFactory.emptySolution());
task.run(); // ...so that when this invokes the success callback, it won't publish an error
// assert
verifyNoInteractions(eventPublisher);
}
@Test
void should_propagate_any_exception_from_solver() {
// arrange
// Prepare a future that will be returned by mock executor
String exceptionMessage = "msg 123";
ListenableFutureTask task = ListenableFutureTask.create(() -> {
throw new TestException(exceptionMessage);
});
when(executor.submit(any(SolverManager.SolvingTask.class))).thenReturn(task);
// act (1)
// Run it synchronously (otherwise the test would be unreliable!)
task.run();
solverManager.startSolver(SolutionFactory.emptySolution());
// assert (1)
verify(eventPublisher).fire(errorEventArgumentCaptor.capture());
assertThat(errorEventArgumentCaptor.getValue().message)
.contains(TestException.class.getName())
.contains(exceptionMessage);
PlanningVisit planningVisit = PlanningVisitFactory.testVisit(1);
PlanningVehicle planningVehicle = PlanningVehicleFactory.testVehicle(1);
// act & assert (2)
assertTestExceptionThrownDuringOperation(() -> solverManager.addVisit(planningVisit));
assertTestExceptionThrownDuringOperation(() -> solverManager.removeVisit(planningVisit));
assertTestExceptionThrownDuringOperation(() -> solverManager.addVehicle(planningVehicle));
assertTestExceptionThrownDuringOperation(() -> solverManager.removeVehicle(planningVehicle));
assertTestExceptionThrownWhenStoppingSolver(solverManager);
}
private static void assertTestExceptionThrownDuringOperation(ThrowingCallable runnable) {
assertTestExceptionThrownDuring(runnable, "died");
}
private static void assertTestExceptionThrownWhenStoppingSolver(SolverManager routeOptimizer) {
assertTestExceptionThrownDuring(routeOptimizer::stopSolver, "stop");
}
private static void assertTestExceptionThrownDuring(ThrowingCallable runnable, String message) {
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(runnable)
.withMessageContaining(message)
.withCauseInstanceOf(TestException.class);
}
private static class TestException extends RuntimeException {
TestException(String message) {
super(message);
}
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverIntegrationTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.testLocation;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.fromLocation;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit;
import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.emptySolution;
import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.solutionFromVisits;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent;
import org.optaplanner.core.api.solver.event.SolverEventListener;
import org.optaplanner.core.config.solver.SolverConfig;
import org.optaweb.vehiclerouting.plugin.planner.change.AddVisit;
import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class SolverIntegrationTest {
private static final Logger logger = LoggerFactory.getLogger(SolverIntegrationTest.class);
// Set a benevolent timeout to avoid issues in the CI environment.
private static final int PFC_PROPAGATION_TIMEOUT_MILLIS = 10_000;
private SolverConfig solverConfig;
private ExecutorService executor;
private ProblemChangeProcessingMonitor monitor;
private Future futureSolution;
@BeforeEach
void setUp() {
solverConfig = SolverConfig.createFromXmlResource(Constants.SOLVER_CONFIG);
solverConfig.setDaemon(true);
executor = Executors.newSingleThreadExecutor();
monitor = new ProblemChangeProcessingMonitor();
}
@AfterEach
void tearDown() throws InterruptedException {
executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
}
@Disabled("Solver fails fast on empty value ranges") // TODO file an OptaPlanner ticket for empty value ranges
@Test
void solver_in_daemon_mode_should_not_fail_on_empty_solution() {
Solver solver =
SolverFactory. create(solverConfig).buildSolver();
assertThat(solver.solve(emptySolution())).isNotNull();
}
// TODO remove vehicle, change capacity, change demand...
@Test
void removing_visits_should_not_fail() {
long distance = 1;
PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1);
VehicleRoutingSolution solution = solutionFromVisits(
singletonList(vehicle),
new PlanningDepot(testLocation(1, location -> distance)),
singletonList(fromLocation(testLocation(2, location -> distance))));
Solver solver =
SolverFactory. create(solverConfig).buildSolver();
solver.addEventListener(monitor);
startSolver(solver, solution);
for (int id = 3; id < 6; id++) {
logger.info("Add visit ({})", id);
monitor.beforeProblemChange();
solver.addProblemChange(new AddVisit(fromLocation(testLocation(id, location -> distance))));
assertThat(monitor.awaitAllProblemChanges(PFC_PROPAGATION_TIMEOUT_MILLIS)).isTrue();
}
List visitIds = Arrays.asList(5, 2, 3);
for (int id : visitIds) {
logger.info("Remove visit ({})", id);
assertThat(solver.isEveryProblemChangeProcessed()).isTrue();
monitor.beforeProblemChange();
solver.addProblemChange(new RemoveVisit(testVisit(id)));
assertThat(solver.isEveryProblemChangeProcessed()).isFalse(); // probably not 100% safe
// Notice that it's not possible to check individual problem fact changes completion.
// When we receive a BestSolutionChangedEvent with unprocessed PFCs,
// we don't know how many of them there are.
if (!monitor.awaitAllProblemChanges(PFC_PROPAGATION_TIMEOUT_MILLIS)) {
assertThat(terminateSolver(solver)).isNotNull();
fail("Problem fact change hasn't been completed");
}
}
assertThat(terminateSolver(solver)).isNotNull();
}
private void startSolver(Solver solver, VehicleRoutingSolution solution) {
futureSolution = executor.submit(() -> solver.solve(solution));
}
private VehicleRoutingSolution terminateSolver(Solver solver) {
solver.terminateEarly();
try {
return futureSolution.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Interrupted", e);
} catch (ExecutionException e) {
fail("Solving failed", e);
}
throw new AssertionError();
}
static class ProblemChangeProcessingMonitor implements SolverEventListener {
private static final Logger logger = LoggerFactory.getLogger(ProblemChangeProcessingMonitor.class);
private final Semaphore problemChanges = new Semaphore(0);
void beforeProblemChange() {
int permitsDrained = problemChanges.drainPermits();
logger.debug("Before PFC (permits drained: {})", permitsDrained);
}
boolean awaitAllProblemChanges(int milliseconds) {
// Available permits may rarely be > 0 if the PFC completes before we start waiting,
// or if the solution has improved since we called beforePFC() => the test is not completely reliable.
logger.debug("WAIT (completed PFCs: {})", problemChanges.availablePermits());
try {
if (problemChanges.tryAcquire(milliseconds, TimeUnit.MILLISECONDS)) {
logger.info("Problem Fact Change DONE");
return true;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Interrupted", e);
}
return false;
}
@Override
public void bestSolutionChanged(BestSolutionChangedEvent event) {
// This happens on solver thread
if (!event.isEveryProblemChangeProcessed()) {
logger.debug("UNPROCESSED");
} else if (!event.getNewBestScore().isSolutionInitialized()) {
logger.debug("UNINITIALIZED ({})", event.getNewBestScore());
} else {
logger.debug("New best solution (COMPLETE)");
problemChanges.release();
}
}
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverManagerIntegrationTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.solutionFromVisits;
import java.util.concurrent.Semaphore;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.optaweb.vehiclerouting.plugin.planner.domain.DistanceMap;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import org.optaweb.vehiclerouting.service.route.RouteChangedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
@QuarkusTest
@TestProfile(SolverTestProfile.class)
class SolverManagerIntegrationTest {
@Inject
SolverManager solverManager;
@Inject
RouteChangedEventSemaphore routeChangedEventSemaphore;
private static DistanceMap mockDistanceMap() {
return location -> 60;
}
@Test
@Timeout(value = 60)
void solver_should_be_in_daemon_mode() throws InterruptedException {
PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1);
PlanningLocation depot = PlanningLocationFactory.testLocation(1, mockDistanceMap());
PlanningLocation visit = PlanningLocationFactory.testLocation(2, mockDistanceMap());
VehicleRoutingSolution solution = solutionFromVisits(
singletonList(vehicle),
new PlanningDepot(depot),
singletonList(PlanningVisitFactory.fromLocation(visit)));
solverManager.startSolver(solution);
// Waits until the solution is initialized. There is only 1 possible step => no more than 1 RouteChangedEvent.
routeChangedEventSemaphore.waitForRouteUpdate();
// The best solution has been updated. We know the score must be -1hard/-120soft because that's
// the only possible solution. The termination property is set exactly to this score => we know
// the solver is now terminated.
// If the solver is in daemon mode, it doesn't return from solve() although solving has ended.
// Instead, it's actively waiting for a PFC and will restart once it arrives from the outside (the test thread).
// If it's not in daemon mode, it returns from solve() method once the termination condition is met
// and the following PFC attempt fails.
assertThatCode(() -> solverManager.changeCapacity(vehicle)).doesNotThrowAnyException();
}
@ApplicationScoped
static class RouteChangedEventSemaphore {
private static final Logger logger = LoggerFactory.getLogger(RouteChangedEventSemaphore.class);
private final Semaphore semaphore = new Semaphore(0);
public void onApplicationEvent(@Observes RouteChangedEvent event) {
logger.info("DISTANCE: {}", event.distance());
semaphore.release();
}
void waitForRouteUpdate() throws InterruptedException {
semaphore.acquire();
int remainingPermits = semaphore.availablePermits();
if (remainingPermits > 0) {
throw new IllegalStateException(
"Only 1 RouteChangedEvent was expected but there were at least " + (remainingPermits + 1));
}
}
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverManagerTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.AdditionalAnswers.answer;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer1;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent;
import org.optaweb.vehiclerouting.plugin.planner.change.AddVehicle;
import org.optaweb.vehiclerouting.plugin.planner.change.AddVisit;
import org.optaweb.vehiclerouting.plugin.planner.change.ChangeVehicleCapacity;
import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVehicle;
import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@ExtendWith(MockitoExtension.class)
class SolverManagerTest {
private final VehicleRoutingSolution solution = SolutionFactory.emptySolution();
private final PlanningVehicle testVehicle = PlanningVehicleFactory.testVehicle(1);
private final PlanningVisit testVisit = PlanningVisitFactory.testVisit(1);
@Captor
private ArgumentCaptor solutionArgumentCaptor;
@Mock
private BestSolutionChangedEvent bestSolutionChangedEvent;
@Mock
private ListenableFuture solverFuture;
@Mock
private Solver solver;
@Mock
private ListeningExecutorService executor;
@Mock
private RouteChangedEventPublisher routeChangedEventPublisher;
@InjectMocks
private SolverManager solverManager;
private void returnSolverFutureWhenSolverIsStarted() {
// always run the runnable submitted to executor (that's what every executor does)
// we can then verify that solver.solve() has been called
when(executor.submit(any(SolverManager.SolvingTask.class))).thenAnswer(
answer((Answer1, SolverManager.SolvingTask>) callable -> {
callable.call();
return solverFuture;
}));
}
@Test
void should_listen_for_best_solution_events() {
verify(solver).addEventListener(solverManager);
}
@Test
void ignore_new_best_solutions_when_unprocessed_fact_changes() {
// arrange
when(bestSolutionChangedEvent.isEveryProblemChangeProcessed()).thenReturn(false);
// act
solverManager.bestSolutionChanged(bestSolutionChangedEvent);
// assert
verify(bestSolutionChangedEvent, never()).getNewBestSolution();
verify(routeChangedEventPublisher, never()).publishSolution(any());
}
@Test
void publish_new_best_solution_if_all_fact_changes_processed() {
VehicleRoutingSolution solution = SolutionFactory.emptySolution();
when(bestSolutionChangedEvent.isEveryProblemChangeProcessed()).thenReturn(true);
when(bestSolutionChangedEvent.getNewBestSolution()).thenReturn(solution);
solverManager.bestSolutionChanged(bestSolutionChangedEvent);
verify(routeChangedEventPublisher).publishSolution(solutionArgumentCaptor.capture());
VehicleRoutingSolution event = solutionArgumentCaptor.getValue();
assertThat(event).isSameAs(solution);
}
@Test
void startSolver_should_start_solver() {
returnSolverFutureWhenSolverIsStarted();
solverManager.startSolver(solution);
verify(solver).solve(solution);
// cannot start solver that is already solving
assertThatIllegalStateException()
.isThrownBy(() -> solverManager.startSolver(solution));
}
@Test
void stopSolver_should_terminate_solver() {
returnSolverFutureWhenSolverIsStarted();
solverManager.startSolver(solution);
solverManager.stopSolver();
verify(solver).terminateEarly();
// another stopSolver() does nothing
solverManager.stopSolver();
// This verifies there were no more invocations of terminateEarly() without clearing all invocations.
// Not using Mockito.clearInvocations() only because it doesn't like generic arguments.
verify(solver).terminateEarly();
}
@Test
void reset_interrupted_flag() throws ExecutionException, InterruptedException {
returnSolverFutureWhenSolverIsStarted();
// start solver
solverManager.startSolver(solution);
when(solverFuture.isDone()).thenReturn(true);
when(solverFuture.get()).thenThrow(InterruptedException.class);
PlanningVisit visit = testVisit(0);
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> solverManager.removeVisit(visit));
assertThat(Thread.interrupted()).isTrue();
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> solverManager.stopSolver());
assertThat(Thread.interrupted()).isTrue();
}
@Test
void change_operations_should_fail_if_solver_has_not_started_yet() {
assertThatIllegalStateException()
.isThrownBy(() -> solverManager.addVehicle(testVehicle))
.withMessageContaining("started");
assertThatIllegalStateException()
.isThrownBy(() -> solverManager.removeVehicle(testVehicle))
.withMessageContaining("started");
assertThatIllegalStateException()
.isThrownBy(() -> solverManager.changeCapacity(testVehicle))
.withMessageContaining("started");
assertThatIllegalStateException()
.isThrownBy(() -> solverManager.addVisit(testVisit))
.withMessageContaining("started");
assertThatIllegalStateException()
.isThrownBy(() -> solverManager.removeVisit(testVisit))
.withMessageContaining("started");
}
@Test
void change_operations_should_fail_is_solver_has_died() throws ExecutionException, InterruptedException {
returnSolverFutureWhenSolverIsStarted();
solverManager.startSolver(solution);
when(solverFuture.isDone()).thenReturn(true);
when(solverFuture.get()).thenThrow(ExecutionException.class);
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> solverManager.addVehicle(testVehicle))
.withMessageContaining("died");
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> solverManager.removeVehicle(testVehicle))
.withMessageContaining("died");
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> solverManager.changeCapacity(testVehicle))
.withMessageContaining("died");
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> solverManager.addVisit(testVisit))
.withMessageContaining("died");
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> solverManager.removeVisit(testVisit))
.withMessageContaining("died");
}
@Test
void change_operations_should_submit_problem_fact_changes_to_solver() {
returnSolverFutureWhenSolverIsStarted();
solverManager.startSolver(solution);
when(solverFuture.isDone()).thenReturn(false);
solverManager.addVehicle(testVehicle);
verify(solver).addProblemChange(any(AddVehicle.class));
solverManager.removeVehicle(testVehicle);
verify(solver).addProblemChange(any(RemoveVehicle.class));
solverManager.changeCapacity(testVehicle);
verify(solver).addProblemChange(any(ChangeVehicleCapacity.class));
solverManager.addVisit(testVisit);
verify(solver).addProblemChange(any(AddVisit.class));
solverManager.removeVisit(testVisit);
verify(solver).addProblemChange(any(RemoveVisit.class));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverTestProfile.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import java.util.HashMap;
import java.util.Map;
import io.quarkus.test.junit.QuarkusTestProfile;
public class SolverTestProfile implements QuarkusTestProfile {
@Override
public Map getConfigOverrides() {
HashMap config = new HashMap<>();
config.put("quarkus.optaplanner.solver.termination.best-score-limit", "-1hard/-120soft");
return config;
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/VehicleRoutingConstraintProviderTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.testLocation;
import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.fromLocation;
import org.junit.jupiter.api.Test;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;
import org.optaweb.vehiclerouting.plugin.planner.domain.DistanceMap;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit;
import org.optaweb.vehiclerouting.plugin.planner.domain.Standstill;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
class VehicleRoutingConstraintProviderTest {
private final ConstraintVerifier constraintVerifier =
ConstraintVerifier.build(
new VehicleRoutingConstraintProvider(),
VehicleRoutingSolution.class,
Standstill.class,
PlanningVisit.class);
private static DistanceMap distanceToAll(long distance) {
return location -> distance;
}
private static void route(PlanningVehicle vehicle, PlanningVisit... visits) {
Standstill previousStandstill = vehicle;
for (PlanningVisit visit : visits) {
visit.setVehicle(vehicle);
visit.setPreviousStandstill(previousStandstill);
previousStandstill.setNextVisit(visit);
previousStandstill = visit;
}
}
@Test
void vehicle_capacity_penalized_1vehicle_1visit() {
int demand = 100;
int capacity = 5;
PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1, capacity);
vehicle.setDepot(new PlanningDepot(testLocation(1, distanceToAll(0))));
PlanningVisit visit = fromLocation(testLocation(2, distanceToAll(0)), demand);
route(vehicle, visit);
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity)
.given(visit)
.penalizesBy(demand - capacity);
}
@Test
void vehicle_capacity_penalized_1vehicle_3visits() {
int demand1 = 4;
int demand2 = 3;
int demand3 = 9;
int capacity = 5;
PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1, capacity);
vehicle.setDepot(new PlanningDepot(testLocation(0, distanceToAll(0))));
PlanningVisit visit1 = fromLocation(testLocation(1, distanceToAll(0)), demand1);
PlanningVisit visit2 = fromLocation(testLocation(2, distanceToAll(0)), demand2);
PlanningVisit visit3 = fromLocation(testLocation(3, distanceToAll(0)), demand3);
route(vehicle, visit1, visit2, visit3);
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity)
.given(visit1, visit2, visit3)
.penalizesBy(demand1 + demand2 + demand3 - capacity);
}
@Test
void capacity_not_penalized_when_greater_or_equal_to_demand() {
int demand1 = 4;
int demand2 = 3;
int demand3 = 9;
int totalDemand = demand1 + demand2 + demand3;
PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1, totalDemand);
vehicle.setDepot(new PlanningDepot(testLocation(0, distanceToAll(0))));
PlanningVisit visit1 = fromLocation(testLocation(1, distanceToAll(0)), demand1);
PlanningVisit visit2 = fromLocation(testLocation(2, distanceToAll(0)), demand2);
PlanningVisit visit3 = fromLocation(testLocation(3, distanceToAll(0)), demand3);
route(vehicle, visit1, visit2, visit3);
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity)
.given(visit1, visit2, visit3)
.penalizesBy(0);
// test values near the constraint boundary
vehicle.setCapacity(totalDemand + 1);
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity)
.given(visit1, visit2, visit3)
.penalizesBy(0);
}
@Test
void vehicles_capacity_constraint_should_work_for_multiple_vehicles() {
int demand1a = 11;
int demand1b = 12;
int demand1c = 14;
int demand2a = 3000;
int demand2b = 2500;
int demand2c = 8000;
int capacity1 = demand1a + demand1b + demand1c;
int capacity2 = demand2a + demand2b + demand2c;
PlanningDepot depot = new PlanningDepot(testLocation(0, distanceToAll(0)));
PlanningVehicle vehicle1 = PlanningVehicleFactory.testVehicle(1, capacity1);
vehicle1.setDepot(depot);
PlanningVehicle vehicle2 = PlanningVehicleFactory.testVehicle(2, capacity2);
vehicle2.setDepot(depot);
PlanningVisit visit1 = fromLocation(testLocation(1, distanceToAll(0)), demand1a);
PlanningVisit visit2 = fromLocation(testLocation(2, distanceToAll(0)), demand1b);
PlanningVisit visit3 = fromLocation(testLocation(3, distanceToAll(0)), demand1c);
PlanningVisit visit4 = fromLocation(testLocation(4, distanceToAll(0)), demand2a);
PlanningVisit visit5 = fromLocation(testLocation(5, distanceToAll(0)), demand2b);
PlanningVisit visit6 = fromLocation(testLocation(6, distanceToAll(0)), demand2c);
route(vehicle1, visit1, visit2, visit3);
route(vehicle2, visit4, visit5, visit6);
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity)
.given(visit1, visit2, visit3, visit4, visit5, visit6)
.penalizesBy(0);
vehicle1.setCapacity(capacity1 - 3);
vehicle2.setCapacity(capacity2 - 7);
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity)
.given(visit1, visit2, visit3, visit4, visit5, visit6)
.penalizesBy(10);
}
@Test
void distance_2vehicles() {
int fromDepot1 = 1000;
int fromDepot2 = 2000;
PlanningDepot depot1 = new PlanningDepot(testLocation(0, distanceToAll(fromDepot1)));
PlanningDepot depot2 = new PlanningDepot(testLocation(0, distanceToAll(fromDepot2)));
PlanningVehicle vehicle1 = PlanningVehicleFactory.testVehicle(1, Integer.MAX_VALUE);
vehicle1.setDepot(depot1);
PlanningVehicle vehicle2 = PlanningVehicleFactory.testVehicle(1, Integer.MAX_VALUE);
vehicle2.setDepot(depot2);
int fromA = 17;
int fromB = 11;
int fromC = 37;
int fromD = 123;
int fromE = 77;
int fromF = 99;
PlanningVisit visitA = fromLocation(testLocation(1, distanceToAll(fromA)));
PlanningVisit visitB = fromLocation(testLocation(2, distanceToAll(fromB)));
PlanningVisit visitC = fromLocation(testLocation(3, distanceToAll(fromC)));
PlanningVisit visitD = fromLocation(testLocation(4, distanceToAll(fromD)));
PlanningVisit visitE = fromLocation(testLocation(5, distanceToAll(fromE)));
PlanningVisit visitF = fromLocation(testLocation(6, distanceToAll(fromF)));
route(vehicle1, visitA, visitB, visitC);
route(vehicle2, visitD, visitE, visitF);
// vehicle 1: depot→last
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromPreviousStandstill)
.given(vehicle1, visitA, visitB, visitC)
.penalizesBy(fromDepot1 + fromA + fromB);
// vehicle 1: last→depot
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromLastVisitToDepot)
.given(vehicle1, visitA, visitB, visitC)
.penalizesBy(fromC);
// vehicle 2: depot→last
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromPreviousStandstill)
.given(vehicle2, visitD, visitE, visitF)
.penalizesBy(fromDepot2 + fromD + fromE);
// vehicle 2: last→depot
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromLastVisitToDepot)
.given(vehicle2, visitD, visitE, visitF)
.penalizesBy(fromF);
// vehicles 1+2: depot→last
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromPreviousStandstill)
.given(vehicle1, vehicle2, visitA, visitB, visitC, visitD, visitE, visitF)
.penalizesBy(fromDepot1 + fromDepot2 + fromA + fromB + fromD + fromE);
// vehicles 1+2: last→depot
constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromLastVisitToDepot)
.given(vehicle1, vehicle2, visitA, visitB, visitC, visitD, visitE, visitF)
.penalizesBy(fromC + fromF);
// score
constraintVerifier.verifyThat()
.given(vehicle1, vehicle2, visitA, visitB, visitC, visitD, visitE, visitF)
.scores(HardSoftLongScore.ofSoft(
-(fromDepot1 + fromDepot2 + fromA + fromB + fromC + fromD + fromE + fromF)));
}
}
================================================
FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/change/AddVehicleTest.java
================================================
package org.optaweb.vehiclerouting.plugin.planner.change;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.optaweb.vehiclerouting.plugin.planner.MockSolver;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle;
import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory;
import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution;
class AddVehicleTest {
@Test
void add_vehicle_should_add_vehicle() {
VehicleRoutingSolution solution = SolutionFactory.emptySolution();
MockSolver