diff --git a/README.adoc b/README.adoc index 3e5abc2e..3daa5ddc 100644 --- a/README.adoc +++ b/README.adoc @@ -10,6 +10,7 @@ a|* <> * <> * <> * <> +* <> a|* link:use-cases/school-timetabling/README.adoc[Quarkus] (Java, Maven or Gradle, Quarkus, H2) * link:technology/java-spring-boot/README.adoc[Spring Boot] (Java, Maven or Gradle, Spring Boot, H2) @@ -49,6 +50,7 @@ image::build/quickstarts-showcase/src/main/resources/META-INF/resources/screensh * link:technology/java-spring-boot/README.adoc[Run spring-boot-school-timetabling] (Java, Maven or Gradle, Spring Boot, H2) * link:technology/java-activemq-quarkus/README.adoc[Run activemq-quarkus-school-timetabling] (Java, ActiveMQ, Maven, Quarkus) * link:technology/kotlin-quarkus/README.adoc[Run kotlin-quarkus-school-timetabling] (Kotlin, Maven, Quarkus, H2) +* link:use-cases/vehicle-routing/README.adoc[Run quarkus-vehicle-routing] (Java, Maven or Gradle, Quarkus) Without a UI: @@ -92,6 +94,15 @@ image::build/quickstarts-showcase/src/main/resources/META-INF/resources/screensh * link:use-cases/vaccination-scheduling/README.adoc[Run quarkus-vaccination-scheduling] (Java, Maven, Quarkus) +[[quarkus-vehicle-routing]] +=== Quarkus Vehicle Routing + +Find the most efficient routes for a fleet of vehicles. + +image::build/quickstarts-showcase/src/main/resources/META-INF/resources/screenshot/quarkus-vehicle-routing-screenshot.png[] + +* link:use-cases/vehicle-routing/README.adoc[Run quarkus-vehicle-routing] (Java, Maven, Quarkus) + [[optaweb-vehicle-routing]] === OptaWeb Vehicle Routing diff --git a/build/optaplanner-distribution/src/main/assembly/assembly-optaplanner-quickstarts.xml b/build/optaplanner-distribution/src/main/assembly/assembly-optaplanner-quickstarts.xml index 63f8b165..4ae3a173 100644 --- a/build/optaplanner-distribution/src/main/assembly/assembly-optaplanner-quickstarts.xml +++ b/build/optaplanner-distribution/src/main/assembly/assembly-optaplanner-quickstarts.xml @@ -81,6 +81,13 @@ quarkus-app/** + + ../../use-cases/vehicle-routing/target + binaries/use-cases/vehicle-routing + + quarkus-app/** + + ../../use-cases/call-center/target quickstarts/binaries/use-cases/call-center diff --git a/build/optaplanner-distribution/src/main/assembly/sources.xml b/build/optaplanner-distribution/src/main/assembly/sources.xml index ae4acb31..5060b30d 100644 --- a/build/optaplanner-distribution/src/main/assembly/sources.xml +++ b/build/optaplanner-distribution/src/main/assembly/sources.xml @@ -62,6 +62,15 @@ .gitignore + + false + ../../use-cases/vehicle-routing + sources/use-cases/vehicle-routing + + target/** + .gitignore + + false ../../technology/java-quarkus diff --git a/build/quickstarts-showcase/src/main/java/org/optaplanner/quickstarts/all/rest/QuickstartLauncherResource.java b/build/quickstarts-showcase/src/main/java/org/optaplanner/quickstarts/all/rest/QuickstartLauncherResource.java index fbe5eb8e..88f62417 100644 --- a/build/quickstarts-showcase/src/main/java/org/optaplanner/quickstarts/all/rest/QuickstartLauncherResource.java +++ b/build/quickstarts-showcase/src/main/java/org/optaplanner/quickstarts/all/rest/QuickstartLauncherResource.java @@ -65,7 +65,8 @@ public void setup(@Observes StartupEvent startupEvent) { new QuickstartMeta("facility-location"), new QuickstartMeta("maintenance-scheduling"), new QuickstartMeta("vaccination-scheduling"), - new QuickstartMeta("call-center")); + new QuickstartMeta("call-center"), + new QuickstartMeta("vehicle-routing")); File workingDirectory; try { workingDirectory = new File(".").getCanonicalFile(); diff --git a/build/quickstarts-showcase/src/main/resources/META-INF/resources/app.js b/build/quickstarts-showcase/src/main/resources/META-INF/resources/app.js index 5b6987b7..837cfe91 100644 --- a/build/quickstarts-showcase/src/main/resources/META-INF/resources/app.js +++ b/build/quickstarts-showcase/src/main/resources/META-INF/resources/app.js @@ -158,6 +158,9 @@ $(document).ready(function () { $("#vaccination-scheduling-launch").click(function () { launchQuickstart("vaccination-scheduling"); }); + $("#vehicle-routing-launch").click(function () { + launchQuickstart("vehicle-routing"); + }); $("#exit").click(function () { exit(); }); diff --git a/build/quickstarts-showcase/src/main/resources/META-INF/resources/index.html b/build/quickstarts-showcase/src/main/resources/META-INF/resources/index.html index ee5f1c5d..b5687fab 100644 --- a/build/quickstarts-showcase/src/main/resources/META-INF/resources/index.html +++ b/build/quickstarts-showcase/src/main/resources/META-INF/resources/index.html @@ -119,6 +119,25 @@
+
+
+
+
+ Quarkus Vehicle Routing +
+
+ Screenshot +
+

+ Find the most efficient routes
for a fleet of vehicles. +

+ +
+
+
+
diff --git a/build/quickstarts-showcase/src/main/resources/META-INF/resources/screenshot/quarkus-vehicle-routing-screenshot.png b/build/quickstarts-showcase/src/main/resources/META-INF/resources/screenshot/quarkus-vehicle-routing-screenshot.png new file mode 100644 index 00000000..b69078ce Binary files /dev/null and b/build/quickstarts-showcase/src/main/resources/META-INF/resources/screenshot/quarkus-vehicle-routing-screenshot.png differ diff --git a/pom.xml b/pom.xml index 7f755e2e..bc05f578 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ use-cases/maintenance-scheduling use-cases/call-center use-cases/vaccination-scheduling + use-cases/vehicle-routing build/quickstarts-showcase diff --git a/use-cases/vehicle-routing/.dockerignore b/use-cases/vehicle-routing/.dockerignore new file mode 100644 index 00000000..221ee37f --- /dev/null +++ b/use-cases/vehicle-routing/.dockerignore @@ -0,0 +1,3 @@ +* +!target/*-runner +!target/quarkus-app/* \ No newline at end of file diff --git a/use-cases/vehicle-routing/.gitignore b/use-cases/vehicle-routing/.gitignore new file mode 100644 index 00000000..8edd7873 --- /dev/null +++ b/use-cases/vehicle-routing/.gitignore @@ -0,0 +1,13 @@ +/target +/build +/local + +# Eclipse, Netbeans and IntelliJ files +/.* +!.gitignore +!.dockerignore +!.mvn +/nbproject +/*.ipr +/*.iws +/*.iml diff --git a/use-cases/vehicle-routing/README.adoc b/use-cases/vehicle-routing/README.adoc new file mode 100644 index 00000000..fa46f319 --- /dev/null +++ b/use-cases/vehicle-routing/README.adoc @@ -0,0 +1,70 @@ += Vehicle Routing Problem (Java, Quarkus, Maven) + +== Run the application with live coding + +. Start the application: ++ +[source, shell] +---- +$ mvn quarkus:dev +---- + +. Visit http://localhost:8080 in your browser. + +. Click on the *Solve* button. + +Then try _live coding_: + +. Make some changes in the source code. +. Refresh your browser (F5). + +Notice that those changes are immediately in effect. + +== Package and run the application + +When you're done iterating in `quarkus:dev` mode, run the application as a conventional jar file. + +. Compile it: ++ +[source, shell] +---- +$ mvn package +---- + +. Run it: ++ +[source, shell] +---- +$ java -jar ./target/quarkus-app/quarkus-run.jar +---- ++ +[NOTE] +==== +To run it on port 8081 instead, add `-Dquarkus.http.port=8081`. +==== + +. Visit http://localhost:8080 in your browser. + +== Run a native executable + +. https://quarkus.io/guides/building-native-image#configuring-graalvm[Install GraalVM and gu install the native-image tool] + +. Compile it natively: ++ +[source, shell] +---- +$ mvn package -Dnative -DskipTests +---- + +. Run the native executable: ++ +[source, shell] +---- +$ ./target/*-runner +---- + +. Visit http://localhost:8080 in your browser. + +== More information + +Visit https://www.optaplanner.org/[www.optaplanner.org]. diff --git a/use-cases/vehicle-routing/pom.xml b/use-cases/vehicle-routing/pom.xml new file mode 100644 index 00000000..d2c336a5 --- /dev/null +++ b/use-cases/vehicle-routing/pom.xml @@ -0,0 +1,184 @@ + + + 4.0.0 + + org.acme + optaplanner-quarkus-vehicle-routing-quickstart + 1.0-SNAPSHOT + + + 11 + UTF-8 + + 2.2.0.Final + 8.12.0-SNAPSHOT + + 3.8.1 + 3.0.0-M5 + + + + + + io.quarkus + + quarkus-bom + ${version.io.quarkus} + pom + import + + + org.optaplanner + optaplanner-bom + ${version.org.optaplanner} + pom + import + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-jackson + + + org.optaplanner + optaplanner-quarkus + + + org.optaplanner + optaplanner-quarkus-jackson + + + + + io.quarkus + quarkus-junit5 + test + + + org.optaplanner + optaplanner-test + test + + + + + io.quarkus + quarkus-webjars-locator + + + org.webjars + bootstrap + 4.5.3 + runtime + + + org.webjars + jquery + 3.5.1 + runtime + + + org.webjars + font-awesome + 5.15.1 + runtime + + + org.webjars + leaflet + 1.6.0 + runtime + + + + + + + io.quarkus + quarkus-maven-plugin + ${version.io.quarkus} + + + + build + + + + + + maven-compiler-plugin + ${version.compiler.plugin} + + + maven-surefire-plugin + ${version.surefire.plugin} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + ${version.surefire.plugin} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + + + + + jboss-public-repository-group + https://repository.jboss.org/nexus/content/groups/public/ + + + false + + + true + + + + diff --git a/use-cases/vehicle-routing/src/main/docker/Dockerfile.jvm b/use-cases/vehicle-routing/src/main/docker/Dockerfile.jvm new file mode 100644 index 00000000..507f9dd5 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/docker/Dockerfile.jvm @@ -0,0 +1,57 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the docker image run: +# +# mvn package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t optaplanner/vehicle-routing-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 optaplanner/vehicle-routing-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" optaplanner/vehicle-routing-jvm +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal + +ARG JAVA_PACKAGE=java-11-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ + && microdnf update \ + && microdnf clean all \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 540 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=1001 target/quarkus-app/*.jar /deployments/ +COPY --chown=1001 target/quarkus-app/app/ /deployments/app/ +COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] diff --git a/use-cases/vehicle-routing/src/main/docker/Dockerfile.native b/use-cases/vehicle-routing/src/main/docker/Dockerfile.native new file mode 100644 index 00000000..b483e709 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode +# +# Before building the docker image run: +# +# mvn package -Pnative -Dquarkus.native.container-build=true +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t optaplanner/vehicle-routing . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 optaplanner/vehicle-routing +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/bootstrap/DemoDataBuilder.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/bootstrap/DemoDataBuilder.java new file mode 100644 index 00000000..20b1c422 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/bootstrap/DemoDataBuilder.java @@ -0,0 +1,185 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.bootstrap; + +import org.acme.vehiclerouting.domain.Customer; +import org.acme.vehiclerouting.domain.Depot; +import org.acme.vehiclerouting.domain.Vehicle; +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; +import org.acme.vehiclerouting.domain.location.AirLocation; +import org.acme.vehiclerouting.domain.location.DistanceType; +import org.acme.vehiclerouting.domain.location.Location; + +import java.util.ArrayList; +import java.util.List; +import java.util.PrimitiveIterator; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class DemoDataBuilder { + + private static final AtomicLong sequence = new AtomicLong(); + + private Location southWestCorner; + private Location northEastCorner; + private int customerCount; + private int vehicleCount; + private int depotCount; + private int minDemand; + private int maxDemand; + private int vehicleCapacity; + + private DemoDataBuilder() { + } + + public DemoDataBuilder setSouthWestCorner(Location southWestCorner) { + this.southWestCorner = southWestCorner; + return this; + } + + public DemoDataBuilder setNorthEastCorner(Location northEastCorner) { + this.northEastCorner = northEastCorner; + return this; + } + + public DemoDataBuilder setMinDemand(int minDemand) { + this.minDemand = minDemand; + return this; + } + + public DemoDataBuilder setMaxDemand(int maxDemand) { + this.maxDemand = maxDemand; + return this; + } + + public DemoDataBuilder setCustomerCount(int customerCount) { + this.customerCount = customerCount; + return this; + } + + public DemoDataBuilder setVehicleCount(int vehicleCount) { + this.vehicleCount = vehicleCount; + return this; + } + + public DemoDataBuilder setDepotCount(int depotCount) { + this.depotCount = depotCount; + return this; + } + + public DemoDataBuilder setVehicleCapacity(int vehicleCapacity) { + this.vehicleCapacity = vehicleCapacity; + return this; + } + + public static DemoDataBuilder builder() { + return new DemoDataBuilder(); + } + + public VehicleRoutingSolution build() { + + if (minDemand < 1) { + throw new IllegalStateException("minDemand (" + minDemand + ") must be greater than zero."); + } + if (maxDemand < 1) { + throw new IllegalStateException("maxDemand (" + maxDemand + ") must be greater than zero."); + } + if (minDemand >= maxDemand) { + throw new IllegalStateException("maxDemand (" + maxDemand + ") must be greater than minDemand (" + + minDemand + ")."); + } + if (vehicleCapacity < 1) { + throw new IllegalStateException("Number of vehicleCapacity (" + vehicleCapacity + + ") must be greater than zero."); + } + if (customerCount < 1) { + throw new IllegalStateException( + "Number of customerCount (" + customerCount + ") must be greater than zero."); + } + if (vehicleCount < 1) { + throw new IllegalStateException( + "Number of vehicleCount (" + vehicleCount + ") must be greater than zero."); + } + if (depotCount < 1) { + throw new IllegalStateException( + "Number of depotCount (" + depotCount + ") must be greater than zero."); + } + + if (northEastCorner.getLatitude() <= southWestCorner.getLatitude()) { + throw new IllegalStateException("southWestCorner.getLatitude (" + southWestCorner.getLatitude() + + ") must be greater than southWestCorner.getLatitude(" + + southWestCorner.getLatitude() + ")."); + } + + if (northEastCorner.getLongitude() <= southWestCorner.getLongitude()) { + throw new IllegalStateException( + "southWestCorner.getLongitude (" + southWestCorner.getLongitude() + + ") must be greater than southWestCorner.getLongitude(" + + southWestCorner.getLongitude() + ")."); + } + + String name = "demo"; + DistanceType distanceType = DistanceType.AIR_DISTANCE; + String distanceUnitOfMeasurement = "km"; + + Random random = new Random(0); + PrimitiveIterator.OfDouble latitudes = random + .doubles(southWestCorner.getLatitude(), northEastCorner.getLatitude()).iterator(); + PrimitiveIterator.OfDouble longitudes = random + .doubles(southWestCorner.getLongitude(), northEastCorner.getLongitude()).iterator(); + + PrimitiveIterator.OfInt demand = random.ints(minDemand, maxDemand).iterator(); + + PrimitiveIterator.OfInt depotRandom = random.ints(0, depotCount).iterator(); + + Supplier depotSupplier = () -> new Depot(sequence.incrementAndGet(), new AirLocation( + sequence.incrementAndGet(), latitudes.nextDouble(), longitudes.nextDouble())); + + List depotList = Stream.generate(depotSupplier).limit(depotCount).collect(Collectors.toList()); + + Supplier vehicleSupplier = () -> new Vehicle(sequence.incrementAndGet(), vehicleCapacity, + depotList.get(depotRandom.nextInt())); + + List vehicleList = Stream.generate(vehicleSupplier).limit(vehicleCount) + .collect(Collectors.toList()); + + Supplier customerSupplier = () -> new Customer(sequence.incrementAndGet(), + new AirLocation(sequence.incrementAndGet(), latitudes.nextDouble(), + longitudes.nextDouble()), + demand.nextInt()); + + List customerList = Stream.generate(customerSupplier).limit(customerCount) + .collect(Collectors.toList()); + + List locationList = new ArrayList(); + for (Customer customer : customerList) { + + locationList.add(customer.getLocation()); + } + + for (Depot depot : depotList) { + locationList.add(depot.getLocation()); + } + + return new VehicleRoutingSolution(name, distanceType, distanceUnitOfMeasurement, locationList, + depotList, vehicleList, customerList, southWestCorner, northEastCorner); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/bootstrap/DemoDataGenerator.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/bootstrap/DemoDataGenerator.java new file mode 100644 index 00000000..ffe340e5 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/bootstrap/DemoDataGenerator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.bootstrap; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; + +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; +import org.acme.vehiclerouting.domain.location.AirLocation; +import org.acme.vehiclerouting.persistence.VehicleRoutingSolutionRepository; + +import io.quarkus.runtime.StartupEvent; + +@ApplicationScoped +public class DemoDataGenerator { + + private final VehicleRoutingSolutionRepository repository; + + public DemoDataGenerator(VehicleRoutingSolutionRepository repository) { + this.repository = repository; + } + + public void generateDemoData(@Observes StartupEvent startupEvent) { + VehicleRoutingSolution problem = DemoDataBuilder.builder() + .setMinDemand(1) + .setMaxDemand(2) + .setVehicleCapacity(15) + .setCustomerCount(77) + .setVehicleCount(6) + .setDepotCount(2) + .setSouthWestCorner(new AirLocation(0L, 43.751466, 11.177210)) + .setNorthEastCorner(new AirLocation(0L, 43.809291, 11.290195)) + .build(); + + repository.update(problem); + } +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Customer.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Customer.java new file mode 100644 index 00000000..866c9cf4 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Customer.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain; + +import org.acme.vehiclerouting.domain.location.Location; +import org.acme.vehiclerouting.domain.solver.DepotAngleCustomerDifficultyWeightFactory; +import org.optaplanner.core.api.domain.entity.PlanningEntity; +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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties({ "previousStandstill", "nextCustomer" }) +@PlanningEntity(difficultyWeightFactoryClass = DepotAngleCustomerDifficultyWeightFactory.class) +public class Customer implements Standstill { + + protected Long id; + protected Location location; + protected int demand; + + // Planning variables: changes during planning, between score calculations. + @PlanningVariable(valueRangeProviderRefs = { "vehicleRange", + "customerRange" }, graphType = PlanningVariableGraphType.CHAINED) + protected Standstill previousStandstill; + + // Shadow variables + protected Customer nextCustomer; + protected Vehicle vehicle; + + public Customer() { + } + + public Customer(long id, Location location, int demand) { + this.id = id; + this.location = location; + this.demand = demand; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public Location getLocation() { + return location; + } + + public void setLocation(Location 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 Customer getNextCustomer() { + return nextCustomer; + } + + @Override + public void setNextCustomer(Customer nextCustomer) { + this.nextCustomer = nextCustomer; + } + + @Override + @AnchorShadowVariable(sourceVariableName = "previousStandstill") + public Vehicle getVehicle() { + return vehicle; + } + + public void setVehicle(Vehicle vehicle) { + this.vehicle = vehicle; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + + /** + * @return a positive number, the distance multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getDistanceFromPreviousStandstill() { + if (previousStandstill == null) { + // throw new IllegalStateException("This method must not be called when the + // previousStandstill (" + // + previousStandstill + ") is not initialized yet."); + + return Long.MAX_VALUE; + } + return getDistanceFrom(previousStandstill); + } + + /** + * @param standstill never null + * @return a positive number, the distance multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getDistanceFrom(Standstill standstill) { + return standstill.getLocation().getDistanceTo(location); + } + + /** + * @param standstill never null + * @return a positive number, the distance multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getDistanceTo(Standstill standstill) { + return location.getDistanceTo(standstill.getLocation()); + } + + @Override + public String toString() { + if (location.getName() == null) { + return super.toString(); + } + return location.getName(); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Depot.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Depot.java new file mode 100644 index 00000000..96ab4d17 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Depot.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain; + +import org.acme.vehiclerouting.domain.location.Location; + +public class Depot { + + protected Long id; + protected Location location; + + public Depot() { + } + + public Depot(long id, Location location) { + this.id = id; + this.location = location; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + + /** + * @param standstill never null + * @return a positive number, the distance multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getDistanceTo(Standstill standstill) { + return location.getDistanceTo(standstill.getLocation()); + } + + @Override + public String toString() { + if (location.getName() == null) { + return super.toString(); + } + return location.getName(); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Standstill.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Standstill.java new file mode 100644 index 00000000..5e96523d --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Standstill.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain; + +import org.acme.vehiclerouting.domain.location.Location; +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.variable.InverseRelationShadowVariable; + +@PlanningEntity +public interface Standstill { + + /** + * @return never null + */ + Location getLocation(); + + /** + * @return sometimes null + */ + Vehicle getVehicle(); + + /** + * @return sometimes null + */ + @InverseRelationShadowVariable(sourceVariableName = "previousStandstill") + Customer getNextCustomer(); + + void setNextCustomer(Customer nextCustomer); + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Vehicle.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Vehicle.java new file mode 100644 index 00000000..1906cb82 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/Vehicle.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import org.acme.vehiclerouting.domain.location.Location; + +import java.util.ArrayList; +import java.util.List; + +@JsonIgnoreProperties({ "nextCustomer" }) +public class Vehicle implements Standstill { + + protected Long id; + protected int capacity; + protected Depot depot; + + // Shadow variables + protected Customer nextCustomer; + + public Vehicle() { + } + + public Vehicle(long id, int capacity, Depot depot) { + this.id = id; + this.capacity = capacity; + this.depot = depot; + } + + 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 Depot getDepot() { + return depot; + } + + public void setDepot(Depot depot) { + this.depot = depot; + } + + @Override + public Customer getNextCustomer() { + return nextCustomer; + } + + @Override + public void setNextCustomer(Customer nextCustomer) { + this.nextCustomer = nextCustomer; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + + @Override + @JsonBackReference + public Vehicle getVehicle() { + return this; + } + + @Override + public Location getLocation() { + return depot.getLocation(); + } + + /** + * @param standstill never null + * @return a positive number, the distance multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getDistanceTo(Standstill standstill) { + return depot.getDistanceTo(standstill); + } + + /** + * @return route of the vehicle + */ + public List getRoute() { + + List route = new ArrayList(); + + // add list of customer location + Customer customer = getNextCustomer(); + while (customer != null) { + route.add(customer.getLocation()); + customer = customer.getNextCustomer(); + } + + return route; + } + + public Long getTotalDistance() { + + Long totalDistance = getDistanceTo(this); + // add list of ride location + Customer ride = getNextCustomer(); + Customer lastRide = getNextCustomer(); + while (ride != null) { + totalDistance += ride.getDistanceFromPreviousStandstill(); + lastRide = ride; + ride = ride.getNextCustomer(); + } + + if (lastRide != null) { + totalDistance += lastRide.getDistanceTo(this); + } + return totalDistance; + } + + public String getTotalDistanceKm() { + + long totalDistance = getTotalDistance(); + long km = totalDistance / 10L; + long meter = totalDistance % 10L; + return km + "km " + meter + "m"; + } + + @Override + public String toString() { + Location location = getLocation(); + if (location.getName() == null) { + return super.toString(); + } + return location.getName() + "/" + super.toString(); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/VehicleRoutingConstraintProvider.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/VehicleRoutingConstraintProvider.java new file mode 100644 index 00000000..c93957f3 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/VehicleRoutingConstraintProvider.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain; + +import static org.optaplanner.core.api.score.stream.ConstraintCollectors.sum; + +import org.acme.vehiclerouting.domain.timewindowed.TimeWindowedCustomer; +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; + +public class VehicleRoutingConstraintProvider implements ConstraintProvider { + + @Override + public Constraint[] defineConstraints(ConstraintFactory factory) { + return new Constraint[] { vehicleCapacity(factory), distanceToPreviousStandstill(factory), + distanceFromLastCustomerToDepot(factory), arrivalAfterDueTime(factory) }; + } + + // ************************************************************************ + // Hard constraints + // ************************************************************************ + + protected Constraint vehicleCapacity(ConstraintFactory factory) { + return factory.from(Customer.class).groupBy(Customer::getVehicle, sum(Customer::getDemand)) + .filter((vehicle, demand) -> demand > vehicle.getCapacity()) + .penalizeLong("vehicleCapacity", HardSoftLongScore.ONE_HARD, + (vehicle, demand) -> demand - vehicle.getCapacity()); + } + + // ************************************************************************ + // Soft constraints + // ************************************************************************ + + protected Constraint distanceToPreviousStandstill(ConstraintFactory factory) { + return factory.from(Customer.class).penalizeLong("distanceToPreviousStandstill", + HardSoftLongScore.ONE_SOFT, Customer::getDistanceFromPreviousStandstill); + } + + protected Constraint distanceFromLastCustomerToDepot(ConstraintFactory factory) { + return factory.from(Customer.class).filter(customer -> customer.getNextCustomer() == null).penalizeLong( + "distanceFromLastCustomerToDepot", HardSoftLongScore.ONE_SOFT, + customer -> customer.getDistanceTo(customer.getVehicle())); + } + + // ************************************************************************ + // TimeWindowed: additional hard constraints + // ************************************************************************ + + protected Constraint arrivalAfterDueTime(ConstraintFactory factory) { + return factory.from(TimeWindowedCustomer.class) + .filter(customer -> customer.getArrivalTime() > customer.getDueTime()) + .penalizeLong("arrivalAfterDueTime", HardSoftLongScore.ONE_HARD, + customer -> customer.getArrivalTime() - customer.getDueTime()); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/VehicleRoutingSolution.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/VehicleRoutingSolution.java new file mode 100644 index 00000000..3f9ff5b2 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/VehicleRoutingSolution.java @@ -0,0 +1,205 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain; + +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; + +import static java.util.Collections.emptyList; + +import org.acme.vehiclerouting.bootstrap.DemoDataBuilder; +import org.acme.vehiclerouting.domain.location.AirLocation; +import org.acme.vehiclerouting.domain.location.DistanceType; +import org.acme.vehiclerouting.domain.location.Location; +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 { + + protected String name; + protected DistanceType distanceType; + protected String distanceUnitOfMeasurement; + + @ProblemFactCollectionProperty + protected List locationList; + + @ProblemFactCollectionProperty + protected List depotList; + + @PlanningEntityCollectionProperty + @ValueRangeProvider(id = "vehicleRange") + protected List vehicleList; + + @PlanningEntityCollectionProperty + @ValueRangeProvider(id = "customerRange") + protected List customerList; + + @PlanningScore + protected HardSoftLongScore score; + + protected Location southWestCorner; + protected Location northEastCorner; + + public VehicleRoutingSolution() { + } + + public VehicleRoutingSolution(String name, DistanceType distanceType, String distanceUnitOfMeasurement, + List locationList, List depotList, List vehicleList, List customerList, + Location southWestCorner, Location northEastCorner) { + this.name = name; + this.distanceType = distanceType; + this.distanceUnitOfMeasurement = distanceUnitOfMeasurement; + this.locationList = locationList; + this.depotList = depotList; + this.vehicleList = vehicleList; + this.customerList = customerList; + this.southWestCorner = southWestCorner; + this.northEastCorner = northEastCorner; + } + + public static VehicleRoutingSolution empty() { + + VehicleRoutingSolution problem = DemoDataBuilder.builder().setMinDemand(1).setMaxDemand(2) + .setVehicleCapacity(77).setCustomerCount(77).setVehicleCount(7).setDepotCount(1) + .setSouthWestCorner(new AirLocation(0L, 51.44, -0.16)) + .setNorthEastCorner(new AirLocation(0L, 51.56, -0.01)).build(); + + problem.setScore(HardSoftLongScore.ZERO); + + return problem; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public DistanceType getDistanceType() { + return distanceType; + } + + public void setDistanceType(DistanceType distanceType) { + this.distanceType = distanceType; + } + + public String getDistanceUnitOfMeasurement() { + return distanceUnitOfMeasurement; + } + + public void setDistanceUnitOfMeasurement(String distanceUnitOfMeasurement) { + this.distanceUnitOfMeasurement = distanceUnitOfMeasurement; + } + + public List getLocationList() { + return locationList; + } + + public void setLocationList(List locationList) { + this.locationList = locationList; + } + + public List getDepotList() { + return depotList; + } + + public void setDepotList(List depotList) { + this.depotList = depotList; + } + + public List getVehicleList() { + return vehicleList; + } + + public void setVehicleList(List vehicleList) { + this.vehicleList = vehicleList; + } + + public List getCustomerList() { + return customerList; + } + + public void setCustomerList(List customerList) { + this.customerList = customerList; + } + + public HardSoftLongScore getScore() { + return score; + } + + public void setScore(HardSoftLongScore score) { + this.score = score; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + + public List getBounds() { + return Arrays.asList(southWestCorner, northEastCorner); + } + + public String getDistanceString(NumberFormat numberFormat) { + if (score == null) { + return null; + } + long distance = -score.getSoftScore(); + if (distanceUnitOfMeasurement == null) { + return numberFormat.format(((double) distance) / 1000.0); + } + switch (distanceUnitOfMeasurement) { + case "sec": // TODO why are the values 1000 larger? + long hours = distance / 3600000L; + long minutes = distance % 3600000L / 60000L; + long seconds = distance % 60000L / 1000L; + long milliseconds = distance % 1000L; + return hours + "h " + minutes + "m " + seconds + "s " + milliseconds + "ms"; + case "km": { // TODO why are the values 1000 larger? + long km = distance / 1000L; + long meter = distance % 1000L; + return km + "km " + meter + "m"; + } + case "meter": { + long km = distance / 1000L; + long meter = distance % 1000L; + return km + "km " + meter + "m"; + } + default: + return numberFormat.format(((double) distance) / 1000.0) + " " + distanceUnitOfMeasurement; + } + } + + public String getDistanceKm() { + if (score == null) { + return null; + } + long distance = -score.getSoftScore(); + long totalMeter = distance * 100; + long km = totalMeter / 1000L; + long meter = totalMeter % 1000L; + return km + "km " + meter + "m"; + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/AirLocation.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/AirLocation.java new file mode 100644 index 00000000..94ae51ce --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/AirLocation.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.location; + +/** + * The cost between 2 locations is a straight line: the euclidean distance + * between their GPS coordinates. Used with {@link DistanceType#AIR_DISTANCE}. + */ + +public class AirLocation extends Location { + + public AirLocation() { + } + + public AirLocation(long id, double latitude, double longitude) { + super(id, latitude, longitude); + } + + @Override + public long getDistanceTo(Location location) { + double distance = getAirDistanceDoubleTo(location); + // Multiplied by 1000 to avoid floating point arithmetic rounding errors + return (long) (distance * 1000.0 + 0.5); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/DistanceType.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/DistanceType.java new file mode 100644 index 00000000..9c2bf6f7 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/DistanceType.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.location; + + +public enum DistanceType { + /** + * Requires that all {@link Location} instances are of type {@link AirLocation}. + */ + AIR_DISTANCE, + /** + * Requires that all {@link Location} instances are of type {@link RoadLocation}. + */ + ROAD_DISTANCE, + /** + * Requires that all {@link Location} instances are of type {@link RoadSegmentLocation} or {@link HubSegmentLocation}. + */ + SEGMENTED_ROAD_DISTANCE; +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/Location.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/Location.java new file mode 100644 index 00000000..6dadc78b --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/Location.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.location; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonFormat(shape = JsonFormat.Shape.ARRAY) +@JsonIgnoreProperties({ "id", "name" }) +public abstract class Location { + + protected Long id = null; + protected String name = null; + protected double latitude; + protected double longitude; + + public Location() { + } + + public Location(long id, double latitude, double longitude) { + this.id = id; + this.latitude = latitude; + this.longitude = longitude; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + + /** + * The distance's unit of measurement depends on the + * {@link VehicleRoutingSolution}'s {@link DistanceType}. It can be in miles or + * km, but for most cases it's in the TSPLIB's unit of measurement. + * + * @param location never null + * @return a positive number, the distance multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public abstract long getDistanceTo(Location location); + + public double getAirDistanceDoubleTo(Location location) { + // Implementation specified by TSPLIB + // http://www2.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95/ + // Euclidean distance (Pythagorean theorem) - not correct when the surface is a + // sphere + double latitudeDifference = location.latitude - latitude; + double longitudeDifference = location.longitude - longitude; + return Math.sqrt((latitudeDifference * latitudeDifference) + (longitudeDifference * longitudeDifference)); + } + + /** + * The angle relative to the direction EAST. + * + * @param location never null + * @return in Cartesian coordinates + */ + public double getAngle(Location 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() { + if (name == null) { + return super.toString(); + } + return name; + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/RoadLocation.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/RoadLocation.java new file mode 100644 index 00000000..29dc8b09 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/RoadLocation.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.location; + +import java.util.Map; + + +/** + * The cost between 2 locations was precalculated on a real road network route. + * The cost itself might be the distance in km, the travel time, the fuel usage or a weighted function of any of those. + * Used with {@link DistanceType#ROAD_DISTANCE}. + */ + +public class RoadLocation extends Location { + + // Prefer Map over array or List because customers might be added and removed in real-time planning. + protected Map travelDistanceMap; + + public RoadLocation() { + } + + public RoadLocation(long id, double latitude, double longitude) { + super(id, latitude, longitude); + } + + public Map getTravelDistanceMap() { + return travelDistanceMap; + } + + public void setTravelDistanceMap(Map travelDistanceMap) { + this.travelDistanceMap = travelDistanceMap; + } + + @Override + public long getDistanceTo(Location location) { + if (this == location) { + return 0L; + } + double distance = travelDistanceMap.get((RoadLocation) location); + // Multiplied by 1000 to avoid floating point arithmetic rounding errors + return (long) (distance * 1000.0 + 0.5); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/segmented/HubSegmentLocation.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/segmented/HubSegmentLocation.java new file mode 100644 index 00000000..a75f3e52 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/segmented/HubSegmentLocation.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.location.segmented; + +import java.util.Map; + +import org.acme.vehiclerouting.domain.location.Location; + + +/** + * Assistant for {@link RoadSegmentLocation}. + * Used with {@link DistanceType#SEGMENTED_ROAD_DISTANCE}. + */ + +public class HubSegmentLocation extends Location { + + // Prefer Map over array or List because customers might be added and removed in real-time planning. + protected Map nearbyTravelDistanceMap; + protected Map hubTravelDistanceMap; + + public HubSegmentLocation() { + } + + public HubSegmentLocation(long id, double latitude, double longitude) { + super(id, latitude, longitude); + } + + public Map getNearbyTravelDistanceMap() { + return nearbyTravelDistanceMap; + } + + public void setNearbyTravelDistanceMap(Map nearbyTravelDistanceMap) { + this.nearbyTravelDistanceMap = nearbyTravelDistanceMap; + } + + public Map getHubTravelDistanceMap() { + return hubTravelDistanceMap; + } + + public void setHubTravelDistanceMap(Map hubTravelDistanceMap) { + this.hubTravelDistanceMap = hubTravelDistanceMap; + } + + @Override + public long getDistanceTo(Location location) { + double distance; + if (location instanceof RoadSegmentLocation) { + distance = getDistanceDouble((RoadSegmentLocation) location); + } else { + distance = hubTravelDistanceMap.get((HubSegmentLocation) location); + } + // Multiplied by 1000 to avoid floating point arithmetic rounding errors + return (long) (distance * 1000.0 + 0.5); + } + + public double getDistanceDouble(RoadSegmentLocation location) { + Double distance = nearbyTravelDistanceMap.get(location); + if (distance == null) { + // location isn't nearby + distance = getShortestDistanceDoubleThroughOtherHub(location); + } + return distance; + } + + protected double getShortestDistanceDoubleThroughOtherHub(RoadSegmentLocation location) { + double shortestDistance = Double.MAX_VALUE; + // Don't use location.getHubTravelDistanceMap().keySet() instead because distances aren't always paired + for (Map.Entry otherHubEntry : hubTravelDistanceMap.entrySet()) { + HubSegmentLocation otherHub = otherHubEntry.getKey(); + Double otherHubNearbyDistance = otherHub.nearbyTravelDistanceMap.get(location); + if (otherHubNearbyDistance != null) { + double distance = otherHubEntry.getValue() + otherHubNearbyDistance; + if (distance < shortestDistance) { + shortestDistance = distance; + } + } + } + return shortestDistance; + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/segmented/RoadSegmentLocation.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/segmented/RoadSegmentLocation.java new file mode 100644 index 00000000..0fc08aac --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/location/segmented/RoadSegmentLocation.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.location.segmented; + +import java.util.Map; + +import org.acme.vehiclerouting.domain.location.Location; + +/** + * Like {@link RoadLocation}, but for high scale problems to avoid the memory + * issue of keeping the entire cost matrix in memory. Used with + * {@link DistanceType#SEGMENTED_ROAD_DISTANCE}. + */ +public class RoadSegmentLocation extends Location { + + // Prefer Map over array or List because customers might be added and removed in + // real-time planning. + protected Map nearbyTravelDistanceMap; + protected Map hubTravelDistanceMap; + + public RoadSegmentLocation() { + } + + public RoadSegmentLocation(long id, double latitude, double longitude) { + super(id, latitude, longitude); + } + + public Map getNearbyTravelDistanceMap() { + return nearbyTravelDistanceMap; + } + + public void setNearbyTravelDistanceMap(Map nearbyTravelDistanceMap) { + this.nearbyTravelDistanceMap = nearbyTravelDistanceMap; + } + + public Map getHubTravelDistanceMap() { + return hubTravelDistanceMap; + } + + public void setHubTravelDistanceMap(Map hubTravelDistanceMap) { + this.hubTravelDistanceMap = hubTravelDistanceMap; + } + + @Override + public long getDistanceTo(Location location) { + Double distance = getDistanceDouble((RoadSegmentLocation) location); + // Multiplied by 1000 to avoid floating point arithmetic rounding errors + return (long) (distance * 1000.0 + 0.5); + } + + public Double getDistanceDouble(RoadSegmentLocation location) { + Double distance = nearbyTravelDistanceMap.get(location); + if (distance == null) { + // location isn't nearby + distance = getShortestDistanceDoubleThroughHubs(location); + } + return distance; + } + + protected double getShortestDistanceDoubleThroughHubs(RoadSegmentLocation location) { + double shortestDistance = Double.MAX_VALUE; + for (Map.Entry entry : hubTravelDistanceMap.entrySet()) { + double distance = entry.getValue(); + distance += entry.getKey().getDistanceDouble(location); + if (distance < shortestDistance) { + shortestDistance = distance; + } + } + return shortestDistance; + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/DepotAngleCustomerDifficultyWeightFactory.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/DepotAngleCustomerDifficultyWeightFactory.java new file mode 100644 index 00000000..07637ed7 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/DepotAngleCustomerDifficultyWeightFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.solver; + +import static java.util.Comparator.comparingDouble; +import static java.util.Comparator.comparingLong; + +import java.util.Comparator; + +import org.acme.vehiclerouting.domain.Customer; +import org.acme.vehiclerouting.domain.Depot; +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; +import org.optaplanner.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; + +/** + * On large datasets, the constructed solution looks like pizza slices. + */ +public class DepotAngleCustomerDifficultyWeightFactory + implements SelectionSorterWeightFactory { + + @Override + public DepotAngleCustomerDifficultyWeight createSorterWeight(VehicleRoutingSolution vehicleRoutingSolution, + Customer customer) { + Depot depot = vehicleRoutingSolution.getDepotList().get(0); + return new DepotAngleCustomerDifficultyWeight(customer, + customer.getLocation().getAngle(depot.getLocation()), + customer.getLocation().getDistanceTo(depot.getLocation()) + + depot.getLocation().getDistanceTo(customer.getLocation())); + } + + public static class DepotAngleCustomerDifficultyWeight + implements Comparable { + + private static final Comparator COMPARATOR = comparingDouble( + (DepotAngleCustomerDifficultyWeight weight) -> weight.depotAngle) + .thenComparingLong(weight -> weight.depotRoundTripDistance) // Ascending (further from the depot are more difficult) + .thenComparing(weight -> weight.customer, comparingLong(Customer::getId)); + + private final Customer customer; + private final double depotAngle; + private final long depotRoundTripDistance; + + public DepotAngleCustomerDifficultyWeight(Customer customer, + double depotAngle, long depotRoundTripDistance) { + this.customer = customer; + this.depotAngle = depotAngle; + this.depotRoundTripDistance = depotRoundTripDistance; + } + + @Override + public int compareTo(DepotAngleCustomerDifficultyWeight other) { + return COMPARATOR.compare(this, other); + } + } +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/DepotDistanceCustomerDifficultyWeightFactory.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/DepotDistanceCustomerDifficultyWeightFactory.java new file mode 100644 index 00000000..2becab0f --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/DepotDistanceCustomerDifficultyWeightFactory.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.solver; + +import static java.util.Comparator.comparingLong; + +import java.util.Comparator; + +import org.acme.vehiclerouting.domain.Customer; +import org.acme.vehiclerouting.domain.Depot; +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; +import org.optaplanner.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; + +/** + * On large datasets, the constructed solution looks like a Matryoshka doll. + */ +public class DepotDistanceCustomerDifficultyWeightFactory + implements SelectionSorterWeightFactory { + + @Override + public DepotDistanceCustomerDifficultyWeight createSorterWeight(VehicleRoutingSolution vehicleRoutingSolution, + Customer customer) { + Depot depot = vehicleRoutingSolution.getDepotList().get(0); + return new DepotDistanceCustomerDifficultyWeight(customer, + customer.getLocation().getDistanceTo(depot.getLocation()) + + depot.getLocation().getDistanceTo(customer.getLocation())); + } + + public static class DepotDistanceCustomerDifficultyWeight + implements Comparable { + + private static final Comparator COMPARATOR = + // Ascending (further from the depot are more difficult) + comparingLong((DepotDistanceCustomerDifficultyWeight weight) -> weight.depotRoundTripDistance) + .thenComparingInt(weight -> weight.customer.getDemand()) + .thenComparingDouble(weight -> weight.customer.getLocation().getLatitude()) + .thenComparingDouble(weight -> weight.customer.getLocation().getLongitude()) + .thenComparing(weight -> weight.customer, comparingLong(Customer::getId)); + + private final Customer customer; + private final long depotRoundTripDistance; + + public DepotDistanceCustomerDifficultyWeight(Customer customer, long depotRoundTripDistance) { + this.customer = customer; + this.depotRoundTripDistance = depotRoundTripDistance; + } + + @Override + public int compareTo(DepotDistanceCustomerDifficultyWeight other) { + return COMPARATOR.compare(this, other); + } + } +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/LatitudeCustomerDifficultyComparator.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/LatitudeCustomerDifficultyComparator.java new file mode 100644 index 00000000..4de3c208 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/LatitudeCustomerDifficultyComparator.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.solver; + +import java.util.Comparator; + +import org.acme.vehiclerouting.domain.Customer; + +/** + * On large datasets, the constructed solution looks like a zebra crossing. + */ +public class LatitudeCustomerDifficultyComparator implements Comparator { + + private static final Comparator COMPARATOR = Comparator + .comparingDouble((Customer customer) -> customer.getLocation().getLatitude()) + .thenComparingDouble(customer -> customer.getLocation().getLongitude()) + .thenComparingInt(Customer::getDemand).thenComparingLong(Customer::getId); + + @Override + public int compare(Customer a, Customer b) { + return COMPARATOR.compare(a, b); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/nearby/CustomerNearbyDistanceMeter.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/nearby/CustomerNearbyDistanceMeter.java new file mode 100644 index 00000000..cc5b38d9 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/solver/nearby/CustomerNearbyDistanceMeter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.solver.nearby; + +import org.acme.vehiclerouting.domain.Customer; +import org.acme.vehiclerouting.domain.Standstill; +import org.optaplanner.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; + +public class CustomerNearbyDistanceMeter implements NearbyDistanceMeter { + + @Override + public double getNearbyDistance(Customer origin, Standstill destination) { + long distance = origin.getDistanceTo(destination); + // If arriving early also inflicts a cost (more than just not using the vehicle + // more), such as the driver's wage, use this: + // if (origin instanceof TimeWindowedCustomer && destination instanceof + // TimeWindowedCustomer) { + // distance += ((TimeWindowedCustomer) + // origin).getTimeWindowGapTo((TimeWindowedCustomer) destination); + // } + return distance; + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedCustomer.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedCustomer.java new file mode 100644 index 00000000..b70af191 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedCustomer.java @@ -0,0 +1,148 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.timewindowed; + +import org.acme.vehiclerouting.domain.Customer; +import org.acme.vehiclerouting.domain.location.Location; +import org.acme.vehiclerouting.domain.timewindowed.solver.ArrivalTimeUpdatingVariableListener; +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.variable.CustomShadowVariable; +import org.optaplanner.core.api.domain.variable.PlanningVariableReference; + +@PlanningEntity +public class TimeWindowedCustomer extends Customer { + + // Times are multiplied by 1000 to avoid floating point arithmetic rounding + // errors + private long readyTime; + private long dueTime; + private long serviceDuration; + + // Shadow variable + private Long arrivalTime; + + public TimeWindowedCustomer() { + } + + public TimeWindowedCustomer(long id, Location location, int demand, long readyTime, long dueTime, + long serviceDuration) { + super(id, location, demand); + this.readyTime = readyTime; + this.dueTime = dueTime; + this.serviceDuration = serviceDuration; + } + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getReadyTime() { + return readyTime; + } + + public void setReadyTime(long readyTime) { + this.readyTime = readyTime; + } + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getDueTime() { + return dueTime; + } + + public void setDueTime(long dueTime) { + this.dueTime = dueTime; + } + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getServiceDuration() { + return serviceDuration; + } + + public void setServiceDuration(long serviceDuration) { + this.serviceDuration = serviceDuration; + } + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + @CustomShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, + // Arguable, to adhere to API specs (although this works), nextCustomer should + // also be a source, + // because this shadow must be triggered after nextCustomer (but there is no + // need to be triggered by nextCustomer) + sources = { @PlanningVariableReference(variableName = "previousStandstill") }) + public Long getArrivalTime() { + return arrivalTime; + } + + public void setArrivalTime(Long arrivalTime) { + this.arrivalTime = arrivalTime; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public Long getDepartureTime() { + if (arrivalTime == null) { + return null; + } + return Math.max(arrivalTime, readyTime) + serviceDuration; + } + + public boolean isArrivalBeforeReadyTime() { + return arrivalTime != null && arrivalTime < readyTime; + } + + public boolean isArrivalAfterDueTime() { + return arrivalTime != null && dueTime < arrivalTime; + } + + @Override + public TimeWindowedCustomer getNextCustomer() { + return (TimeWindowedCustomer) super.getNextCustomer(); + } + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating + * point arithmetic rounding errors + */ + public long getTimeWindowGapTo(TimeWindowedCustomer other) { + // dueTime doesn't account for serviceDuration + long latestDepartureTime = dueTime + serviceDuration; + long otherLatestDepartureTime = other.getDueTime() + other.getServiceDuration(); + if (latestDepartureTime < other.getReadyTime()) { + return other.getReadyTime() - latestDepartureTime; + } + if (otherLatestDepartureTime < readyTime) { + return readyTime - otherLatestDepartureTime; + } + return 0L; + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedDepot.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedDepot.java new file mode 100644 index 00000000..025e8a29 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedDepot.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.timewindowed; + +import org.acme.vehiclerouting.domain.Depot; +import org.acme.vehiclerouting.domain.location.Location; + +public class TimeWindowedDepot extends Depot { + + // Times are multiplied by 1000 to avoid floating point arithmetic rounding errors + private long readyTime; + private long dueTime; + + public TimeWindowedDepot() { + } + + public TimeWindowedDepot(long id, Location location, long readyTime, long dueTime) { + super(id, location); + this.readyTime = readyTime; + this.dueTime = dueTime; + } + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating point arithmetic rounding errors + */ + public long getReadyTime() { + return readyTime; + } + + public void setReadyTime(long readyTime) { + this.readyTime = readyTime; + } + + /** + * @return a positive number, the time multiplied by 1000 to avoid floating point arithmetic rounding errors + */ + public long getDueTime() { + return dueTime; + } + + public void setDueTime(long dueTime) { + this.dueTime = dueTime; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedVehicleRoutingSolution.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedVehicleRoutingSolution.java new file mode 100644 index 00000000..e8756b5e --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/TimeWindowedVehicleRoutingSolution.java @@ -0,0 +1,23 @@ +/* + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.timewindowed; + +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; + +public class TimeWindowedVehicleRoutingSolution extends VehicleRoutingSolution { + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/solver/ArrivalTimeUpdatingVariableListener.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/solver/ArrivalTimeUpdatingVariableListener.java new file mode 100644 index 00000000..7ec3afff --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/domain/timewindowed/solver/ArrivalTimeUpdatingVariableListener.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.domain.timewindowed.solver; + +import java.util.Objects; + +import org.acme.vehiclerouting.domain.Customer; +import org.acme.vehiclerouting.domain.Standstill; +import org.acme.vehiclerouting.domain.Vehicle; +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; +import org.acme.vehiclerouting.domain.timewindowed.TimeWindowedCustomer; +import org.acme.vehiclerouting.domain.timewindowed.TimeWindowedDepot; +import org.optaplanner.core.api.domain.variable.VariableListener; +import org.optaplanner.core.api.score.director.ScoreDirector; + +// TODO When this class is added only for TimeWindowedCustomer, use TimeWindowedCustomer instead of Customer +public class ArrivalTimeUpdatingVariableListener implements VariableListener { + + @Override + public void beforeEntityAdded(ScoreDirector scoreDirector, Customer customer) { + // Do nothing + } + + @Override + public void afterEntityAdded(ScoreDirector scoreDirector, Customer customer) { + if (customer instanceof TimeWindowedCustomer) { + updateArrivalTime(scoreDirector, (TimeWindowedCustomer) customer); + } + } + + @Override + public void beforeVariableChanged(ScoreDirector scoreDirector, Customer customer) { + // Do nothing + } + + @Override + public void afterVariableChanged(ScoreDirector scoreDirector, Customer customer) { + if (customer instanceof TimeWindowedCustomer) { + updateArrivalTime(scoreDirector, (TimeWindowedCustomer) customer); + } + } + + @Override + public void beforeEntityRemoved(ScoreDirector scoreDirector, Customer customer) { + // Do nothing + } + + @Override + public void afterEntityRemoved(ScoreDirector scoreDirector, Customer customer) { + // Do nothing + } + + protected void updateArrivalTime(ScoreDirector scoreDirector, + TimeWindowedCustomer sourceCustomer) { + Standstill previousStandstill = sourceCustomer.getPreviousStandstill(); + Long departureTime = previousStandstill == null ? null + : (previousStandstill instanceof TimeWindowedCustomer) + ? ((TimeWindowedCustomer) previousStandstill).getDepartureTime() + : ((TimeWindowedDepot) ((Vehicle) previousStandstill).getDepot()).getReadyTime(); + TimeWindowedCustomer shadowCustomer = sourceCustomer; + Long arrivalTime = calculateArrivalTime(shadowCustomer, departureTime); + while (shadowCustomer != null && !Objects.equals(shadowCustomer.getArrivalTime(), arrivalTime)) { + scoreDirector.beforeVariableChanged(shadowCustomer, "arrivalTime"); + shadowCustomer.setArrivalTime(arrivalTime); + scoreDirector.afterVariableChanged(shadowCustomer, "arrivalTime"); + departureTime = shadowCustomer.getDepartureTime(); + shadowCustomer = shadowCustomer.getNextCustomer(); + arrivalTime = calculateArrivalTime(shadowCustomer, departureTime); + } + } + + private Long calculateArrivalTime(TimeWindowedCustomer customer, Long previousDepartureTime) { + if (customer == null || customer.getPreviousStandstill() == null) { + return null; + } + if (customer.getPreviousStandstill() instanceof Vehicle) { + // PreviousStandstill is the Vehicle, so we leave from the Depot at the best + // suitable time + return Math.max(customer.getReadyTime(), + previousDepartureTime + customer.getDistanceFromPreviousStandstill()); + } + return previousDepartureTime + customer.getDistanceFromPreviousStandstill(); + } + +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/persistence/VehicleRoutingSolutionRepository.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/persistence/VehicleRoutingSolutionRepository.java new file mode 100644 index 00000000..8e84afe8 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/persistence/VehicleRoutingSolutionRepository.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.persistence; + +import java.util.Optional; + +import javax.enterprise.context.ApplicationScoped; + +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; + +@ApplicationScoped +public class VehicleRoutingSolutionRepository { + + private VehicleRoutingSolution vehicleRoutingSolution; + + public Optional solution() { + return Optional.ofNullable(vehicleRoutingSolution); + } + + public void update(VehicleRoutingSolution vehicleRoutingSolution) { + this.vehicleRoutingSolution = vehicleRoutingSolution; + } +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/SolverResource.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/SolverResource.java new file mode 100644 index 00000000..1fe6d68e --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/SolverResource.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.rest; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; +import org.acme.vehiclerouting.persistence.VehicleRoutingSolutionRepository; +import org.optaplanner.core.api.score.ScoreManager; +import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; +import org.optaplanner.core.api.solver.SolverManager; + +@Path("/vrp") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class SolverResource { + + private static final long PROBLEM_ID = 0L; + + private final AtomicReference solverError = new AtomicReference<>(); + + private final VehicleRoutingSolutionRepository repository; + private final SolverManager solverManager; + private final ScoreManager scoreManager; + + public SolverResource(VehicleRoutingSolutionRepository repository, + SolverManager solverManager, + ScoreManager scoreManager) { + this.repository = repository; + this.solverManager = solverManager; + this.scoreManager = scoreManager; + } + + private Status statusFromSolution(VehicleRoutingSolution solution) { + return new Status(solution, scoreManager.explainScore(solution).getSummary(), + solverManager.getSolverStatus(PROBLEM_ID)); + } + + @GET + @Path("status") + public Status status() { + Optional.ofNullable(solverError.getAndSet(null)).ifPresent(throwable -> { + throw new RuntimeException("Solver failed", throwable); + }); + + Optional s1 = repository.solution() ; + + VehicleRoutingSolution s = s1.orElse(VehicleRoutingSolution.empty()); + return statusFromSolution(s); + } + + @POST + @Path("solve") + public void solve() { + Optional maybeSolution = repository.solution(); + maybeSolution.ifPresent( + vehicleRoutingSolution -> solverManager.solveAndListen(PROBLEM_ID, id -> vehicleRoutingSolution, + repository::update, (problemId, throwable) -> solverError.set(throwable))); + } + + @POST + @Path("stopSolving") + public void stopSolving() { + solverManager.terminateEarly(PROBLEM_ID); + } +} diff --git a/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/Status.java b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/Status.java new file mode 100644 index 00000000..9af13221 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/Status.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acme.vehiclerouting.rest; + +import org.acme.vehiclerouting.domain.VehicleRoutingSolution; +import org.optaplanner.core.api.solver.SolverStatus; + +class Status { + public final VehicleRoutingSolution solution; + public final String scoreExplanation; + public final boolean isSolving; + + Status(VehicleRoutingSolution solution, String scoreExplanation, SolverStatus solverStatus) { + this.solution = solution; + this.scoreExplanation = scoreExplanation; + this.isSolving = solverStatus != SolverStatus.NOT_SOLVING; + } +} diff --git a/use-cases/vehicle-routing/src/main/resources/META-INF/resources/app.js b/use-cases/vehicle-routing/src/main/resources/META-INF/resources/app.js new file mode 100644 index 00000000..1d40f6b7 --- /dev/null +++ b/use-cases/vehicle-routing/src/main/resources/META-INF/resources/app.js @@ -0,0 +1,283 @@ +const colors = [ + 'aqua', + 'aquamarine', + 'blue', + 'blueviolet', + 'chocolate', + 'cornflowerblue', + 'crimson', + 'forestgreen', + 'gold', + 'lawngreen', + 'limegreen', + 'maroon', + 'mediumvioletred', + 'orange', + 'slateblue', + 'tomato', +]; +let autoRefreshCount = 0; +let autoRefreshIntervalId = null; + +let initialized = false; +const depotByIdMap = new Map(); +const vehicleByIdMap = new Map(); + +const solveButton = $('#solveButton'); +const stopSolvingButton = $('#stopSolvingButton'); +const vehiclesTable = $('#vehicles'); +const depotsTable = $('#depots'); + +const colorById = (i) => colors[i % colors.length]; +const colorByVehicle = (vehicle) => vehicle === null ? null : colorById(vehicle.id); +const colorByDepot = (depot) => depot === null ? null : colorById(depot.id); + +const defaultIcon = new L.Icon.Default(); +const greyIcon = new L.Icon({ + iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-grey.png', + shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.6.0/images/marker-shadow.png', + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41], +}); + +const fetchHeaders = { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, +}; + +const createCostFormat = (notation) => new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 1, + minimumFractionDigits: 1, + notation, +}); +const shortCostFormat = createCostFormat('compact'); +const longCostFormat = createCostFormat('standard'); + +const getStatus = () => { + fetch('/vrp/status', fetchHeaders) + .then((response) => { + if (!response.ok) { + return handleErrorResponse('Get status failed', response); + } else { + return response.json().then((data) => showProblem(data)); + } + }) + .catch((error) => handleClientError('Failed to process response', error)); +}; + +const solve = () => { + fetch('/vrp/solve', {...fetchHeaders, method: 'POST'}) + .then((response) => { + if (!response.ok) { + return handleErrorResponse('Start solving failed', response); + } else { + updateSolvingStatus(true); + autoRefreshCount = 300; + if (autoRefreshIntervalId == null) { + autoRefreshIntervalId = setInterval(autoRefresh, 500); + } + } + }) + .catch((error) => handleClientError('Failed to process response', error)); +}; + +const stopSolving = () => { + fetch('/vrp/stopSolving', {...fetchHeaders, method: 'POST'}) + .then((response) => { + if (!response.ok) { + return handleErrorResponse('Stop solving failed', response); + } else { + updateSolvingStatus(false); + getStatus(); + } + }) + .catch((error) => handleClientError('Failed to process response', error)); +}; + +const formatErrorResponseBody = (body) => { + // JSON must not contain \t (Quarkus bug) + const json = JSON.parse(body.replace(/\t/g, ' ')); + return `${json.details}\n${json.stack}`; +}; + +const handleErrorResponse = (title, response) => { + return response.text() + .then((body) => { + const message = `${title} (${response.status}: ${response.statusText}).`; + const stackTrace = body ? formatErrorResponseBody(body) : ''; + showError(message, stackTrace); + }); +}; + +const handleClientError = (title, error) => { + console.error(error); + showError(`${title}.`, + // Stack looks differently in Chrome and Firefox. + error.stack.startsWith(error.name) + ? error.stack + : `${error.name}: ${error.message}\n ${error.stack.replace(/\n/g, '\n ')}`); +}; + +const showError = (message, stackTrace) => { + const notification = $(`