Repository: kiegroup/optaweb-vehicle-routing Branch: main Commit: f2390d6d04bb Files: 399 Total size: 761.0 KB Directory structure: gitextract__s5biv9q/ ├── .gitattributes ├── .github/ │ └── dependabot.yml ├── .gitignore ├── .mvn/ │ ├── extensions.xml │ └── wrapper/ │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CONTRIBUTING.adoc ├── CREDITS.adoc ├── LICENSE.txt ├── README.adoc ├── mvnw ├── mvnw.cmd ├── optaweb-vehicle-routing-backend/ │ ├── .dockerignore │ ├── .env-example │ ├── .gitignore │ ├── Dockerfile │ ├── README.adoc │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── docker/ │ │ │ ├── Dockerfile.jvm │ │ │ ├── Dockerfile.legacy-jar │ │ │ ├── Dockerfile.native │ │ │ └── Dockerfile.native-micro │ │ ├── java/ │ │ │ └── org/ │ │ │ └── optaweb/ │ │ │ └── vehiclerouting/ │ │ │ ├── Profiles.java │ │ │ ├── domain/ │ │ │ │ ├── Coordinates.java │ │ │ │ ├── CountryCodeValidator.java │ │ │ │ ├── Distance.java │ │ │ │ ├── Location.java │ │ │ │ ├── LocationData.java │ │ │ │ ├── Route.java │ │ │ │ ├── RouteWithTrack.java │ │ │ │ ├── RoutingPlan.java │ │ │ │ ├── RoutingProblem.java │ │ │ │ ├── Vehicle.java │ │ │ │ ├── VehicleData.java │ │ │ │ ├── VehicleFactory.java │ │ │ │ └── package-info.java │ │ │ ├── plugin/ │ │ │ │ ├── persistence/ │ │ │ │ │ ├── DistanceCrudRepository.java │ │ │ │ │ ├── DistanceEntity.java │ │ │ │ │ ├── DistanceKey.java │ │ │ │ │ ├── DistanceRepositoryImpl.java │ │ │ │ │ ├── LocationCrudRepository.java │ │ │ │ │ ├── LocationEntity.java │ │ │ │ │ ├── LocationRepositoryImpl.java │ │ │ │ │ ├── VehicleCrudRepository.java │ │ │ │ │ ├── VehicleEntity.java │ │ │ │ │ ├── VehicleRepositoryImpl.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── planner/ │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── DistanceMapImpl.java │ │ │ │ │ ├── RouteChangedEventPublisher.java │ │ │ │ │ ├── RouteOptimizerConfig.java │ │ │ │ │ ├── RouteOptimizerImpl.java │ │ │ │ │ ├── SolverManager.java │ │ │ │ │ ├── VehicleRoutingConstraintProvider.java │ │ │ │ │ ├── change/ │ │ │ │ │ │ ├── AddVehicle.java │ │ │ │ │ │ ├── AddVisit.java │ │ │ │ │ │ ├── ChangeVehicleCapacity.java │ │ │ │ │ │ ├── RemoveVehicle.java │ │ │ │ │ │ ├── RemoveVisit.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── domain/ │ │ │ │ │ │ ├── DistanceMap.java │ │ │ │ │ │ ├── PlanningDepot.java │ │ │ │ │ │ ├── PlanningLocation.java │ │ │ │ │ │ ├── PlanningLocationFactory.java │ │ │ │ │ │ ├── PlanningVehicle.java │ │ │ │ │ │ ├── PlanningVehicleFactory.java │ │ │ │ │ │ ├── PlanningVisit.java │ │ │ │ │ │ ├── PlanningVisitFactory.java │ │ │ │ │ │ ├── SolutionFactory.java │ │ │ │ │ │ ├── Standstill.java │ │ │ │ │ │ ├── VehicleRoutingSolution.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── weight/ │ │ │ │ │ ├── DepotAngleVisitDifficultyWeightFactory.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── rest/ │ │ │ │ │ ├── ClearResource.java │ │ │ │ │ ├── DataSetDownloadResource.java │ │ │ │ │ ├── DemoResource.java │ │ │ │ │ ├── LocationResource.java │ │ │ │ │ ├── RouteEventResource.java │ │ │ │ │ ├── ServerInfoResource.java │ │ │ │ │ ├── VehicleResource.java │ │ │ │ │ └── model/ │ │ │ │ │ ├── PortableCoordinates.java │ │ │ │ │ ├── PortableDistance.java │ │ │ │ │ ├── PortableErrorMessage.java │ │ │ │ │ ├── PortableLocation.java │ │ │ │ │ ├── PortableRoute.java │ │ │ │ │ ├── PortableRoutingPlan.java │ │ │ │ │ ├── PortableRoutingPlanFactory.java │ │ │ │ │ ├── PortableVehicle.java │ │ │ │ │ ├── RoutingProblemInfo.java │ │ │ │ │ └── ServerInfo.java │ │ │ │ └── routing/ │ │ │ │ ├── AirDistanceRouter.java │ │ │ │ ├── Constants.java │ │ │ │ ├── GraphHopperRouter.java │ │ │ │ ├── RoutingConfig.java │ │ │ │ ├── RoutingEngineException.java │ │ │ │ ├── RoutingProperties.java │ │ │ │ └── package-info.java │ │ │ └── service/ │ │ │ ├── demo/ │ │ │ │ ├── DemoProperties.java │ │ │ │ ├── DemoService.java │ │ │ │ ├── RoutingProblemConfig.java │ │ │ │ ├── RoutingProblemList.java │ │ │ │ ├── dataset/ │ │ │ │ │ ├── DataSet.java │ │ │ │ │ ├── DataSetLocation.java │ │ │ │ │ ├── DataSetMarshaller.java │ │ │ │ │ ├── DataSetVehicle.java │ │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ ├── distance/ │ │ │ │ ├── DistanceCalculator.java │ │ │ │ ├── DistanceMatrixImpl.java │ │ │ │ ├── DistanceRepository.java │ │ │ │ ├── RoutingException.java │ │ │ │ └── package-info.java │ │ │ ├── error/ │ │ │ │ ├── ErrorEvent.java │ │ │ │ ├── ErrorListener.java │ │ │ │ ├── ErrorMessage.java │ │ │ │ ├── ErrorMessageConsumer.java │ │ │ │ └── package-info.java │ │ │ ├── location/ │ │ │ │ ├── DistanceMatrix.java │ │ │ │ ├── DistanceMatrixRow.java │ │ │ │ ├── LocationPlanner.java │ │ │ │ ├── LocationRepository.java │ │ │ │ ├── LocationService.java │ │ │ │ └── package-info.java │ │ │ ├── region/ │ │ │ │ ├── BoundingBox.java │ │ │ │ ├── Region.java │ │ │ │ ├── RegionProperties.java │ │ │ │ ├── RegionService.java │ │ │ │ └── package-info.java │ │ │ ├── reload/ │ │ │ │ ├── ReloadService.java │ │ │ │ └── package-info.java │ │ │ ├── route/ │ │ │ │ ├── RouteChangedEvent.java │ │ │ │ ├── RouteListener.java │ │ │ │ ├── Router.java │ │ │ │ ├── ShallowRoute.java │ │ │ │ └── package-info.java │ │ │ └── vehicle/ │ │ │ ├── VehiclePlanner.java │ │ │ ├── VehicleRepository.java │ │ │ ├── VehicleService.java │ │ │ └── package-info.java │ │ └── resources/ │ │ ├── .gitignore │ │ ├── application.properties │ │ ├── org/ │ │ │ └── optaweb/ │ │ │ └── vehiclerouting/ │ │ │ └── service/ │ │ │ └── demo/ │ │ │ └── belgium-cities.yaml │ │ └── solverConfig.xml │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── optaweb/ │ │ └── vehiclerouting/ │ │ ├── TestConfig.java │ │ ├── domain/ │ │ │ ├── CoordinatesTest.java │ │ │ ├── CountryCodeValidatorTest.java │ │ │ ├── DistanceTest.java │ │ │ ├── LocationDataTest.java │ │ │ ├── LocationTest.java │ │ │ ├── RouteTest.java │ │ │ ├── RouteWithTrackTest.java │ │ │ ├── RoutingPlanTest.java │ │ │ ├── VehicleDataTest.java │ │ │ ├── VehicleFactoryTest.java │ │ │ └── VehicleTest.java │ │ ├── plugin/ │ │ │ ├── persistence/ │ │ │ │ ├── DistanceEntityTest.java │ │ │ │ ├── DistanceRepositoryImplTest.java │ │ │ │ ├── DistanceRepositoryIntegrationTest.java │ │ │ │ ├── LocationEntityTest.java │ │ │ │ ├── LocationRepositoryImplTest.java │ │ │ │ ├── LocationRepositoryIntegrationTest.java │ │ │ │ ├── VehicleEntityTest.java │ │ │ │ └── VehicleRepositoryImplTest.java │ │ │ ├── planner/ │ │ │ │ ├── DistanceMapImplTest.java │ │ │ │ ├── MockSolver.java │ │ │ │ ├── RouteChangedEventPublisherTest.java │ │ │ │ ├── RouteOptimizerImplTest.java │ │ │ │ ├── SolverExceptionTest.java │ │ │ │ ├── SolverIntegrationTest.java │ │ │ │ ├── SolverManagerIntegrationTest.java │ │ │ │ ├── SolverManagerTest.java │ │ │ │ ├── SolverTestProfile.java │ │ │ │ ├── VehicleRoutingConstraintProviderTest.java │ │ │ │ ├── change/ │ │ │ │ │ ├── AddVehicleTest.java │ │ │ │ │ ├── AddVisitTest.java │ │ │ │ │ ├── ChangeVehicleCapacityTest.java │ │ │ │ │ ├── RemoveVehicleTest.java │ │ │ │ │ └── RemoveVisitTest.java │ │ │ │ ├── domain/ │ │ │ │ │ ├── PlanningLocationFactoryTest.java │ │ │ │ │ ├── PlanningLocationTest.java │ │ │ │ │ ├── PlanningVehicleFactoryTest.java │ │ │ │ │ ├── PlanningVehicleTest.java │ │ │ │ │ ├── PlanningVisitFactoryTest.java │ │ │ │ │ └── SolutionFactoryTest.java │ │ │ │ └── weight/ │ │ │ │ └── DepotAngleVisitDifficultyWeightFactoryTest.java │ │ │ ├── rest/ │ │ │ │ ├── ClearResourceTest.java │ │ │ │ ├── DataSetDownloadResourceTest.java │ │ │ │ ├── DemoResourceTest.java │ │ │ │ ├── LocationResourceTest.java │ │ │ │ ├── ServerInfoResourceTest.java │ │ │ │ ├── VehicleResourceTest.java │ │ │ │ └── model/ │ │ │ │ ├── PortableCoordinatesTest.java │ │ │ │ ├── PortableDistanceTest.java │ │ │ │ ├── PortableErrorMessageTest.java │ │ │ │ ├── PortableLocationTest.java │ │ │ │ ├── PortableRouteTest.java │ │ │ │ ├── PortableRoutingPlanFactoryTest.java │ │ │ │ └── PortableVehicleTest.java │ │ │ └── routing/ │ │ │ ├── AirDistanceRouterTest.java │ │ │ ├── GraphHopperIntegrationTest.java │ │ │ ├── GraphHopperRouterTest.java │ │ │ └── RoutingConfigTest.java │ │ ├── service/ │ │ │ ├── demo/ │ │ │ │ ├── DemoServiceTest.java │ │ │ │ ├── RoutingProblemListTest.java │ │ │ │ └── dataset/ │ │ │ │ └── DataSetMarshallerTest.java │ │ │ ├── distance/ │ │ │ │ └── DistanceMatrixImplTest.java │ │ │ ├── error/ │ │ │ │ └── ErrorListenerTest.java │ │ │ ├── location/ │ │ │ │ ├── LocationServiceIntegrationTest.java │ │ │ │ └── LocationServiceTest.java │ │ │ ├── region/ │ │ │ │ ├── BoundingBoxTest.java │ │ │ │ ├── RegionPropertiesTest.java │ │ │ │ └── RegionServiceTest.java │ │ │ ├── reload/ │ │ │ │ └── ReloadServiceTest.java │ │ │ ├── route/ │ │ │ │ ├── RouteListenerTest.java │ │ │ │ └── ShallowRouteTest.java │ │ │ └── vehicle/ │ │ │ ├── VehicleServiceIntegrationTest.java │ │ │ └── VehicleServiceTest.java │ │ └── util/ │ │ ├── jackson/ │ │ │ ├── JacksonAssertions.java │ │ │ ├── JsonAssert.java │ │ │ └── ObjectAssert.java │ │ └── junit/ │ │ ├── FileContent.java │ │ └── FileContentExtension.java │ └── resources/ │ ├── mockito-extensions/ │ │ ├── README.adoc │ │ └── org.mockito.plugins.MockMaker │ └── org/ │ └── optaweb/ │ └── vehiclerouting/ │ ├── plugin/ │ │ ├── rest/ │ │ │ └── model/ │ │ │ ├── portable-error-message.json │ │ │ ├── portable-location.json │ │ │ └── portable-route.json │ │ └── routing/ │ │ ├── CREDITS.adoc │ │ └── planet_12.032,53.0171_12.1024,53.0491.osm.pbf │ └── service/ │ └── demo/ │ └── dataset/ │ └── test-belgium.yaml ├── optaweb-vehicle-routing-distribution/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ └── main/ │ ├── assembly/ │ │ └── assembly-optaweb-vehicle-routing.xml │ └── resources/ │ └── README.adoc ├── optaweb-vehicle-routing-docs/ │ ├── .gitignore │ ├── pom.xml │ └── src/ │ └── main/ │ ├── asciidoc/ │ │ ├── appendix-backend-architecture.adoc │ │ ├── appendix-backend-config.adoc │ │ ├── attributes.adoc │ │ ├── contributing.adoc │ │ ├── development-guide.adoc │ │ ├── index.adoc │ │ ├── introduction.adoc │ │ ├── modules.dot │ │ ├── quickstart.adoc │ │ ├── run-locally.adoc │ │ ├── run-noscript.adoc │ │ ├── run-openshift.adoc │ │ └── user-guide.adoc │ └── assembly/ │ └── assembly-generated-docs-zip.xml ├── optaweb-vehicle-routing-frontend/ │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── README.adoc │ ├── cypress/ │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── fixtures/ │ │ │ ├── example.json │ │ │ ├── response-garz.json │ │ │ └── response-hoppenrade.json │ │ ├── integration/ │ │ │ └── fromLocationsToRoute.js │ │ ├── plugins/ │ │ │ └── index.js │ │ └── support/ │ │ ├── commands.js │ │ └── index.js │ ├── cypress.json │ ├── docker/ │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── default.conf │ │ └── nginx.conf │ ├── package.json │ ├── pom.xml │ ├── public/ │ │ ├── index.html │ │ └── manifest.json │ ├── src/ │ │ ├── @types/ │ │ │ └── eventsourcemock.d.ts │ │ ├── common.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── registerServiceWorker.ts │ │ ├── setupTests.ts │ │ ├── store/ │ │ │ ├── client/ │ │ │ │ ├── actions.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── operations.ts │ │ │ │ ├── reducers.ts │ │ │ │ └── types.ts │ │ │ ├── demo/ │ │ │ │ ├── actions.ts │ │ │ │ ├── demo.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── operations.ts │ │ │ │ ├── reducers.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── message/ │ │ │ │ ├── actions.ts │ │ │ │ ├── index.ts │ │ │ │ ├── message.test.ts │ │ │ │ ├── reducers.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── types.ts │ │ │ ├── mockStore.ts │ │ │ ├── route/ │ │ │ │ ├── actions.ts │ │ │ │ ├── index.ts │ │ │ │ ├── operations.ts │ │ │ │ ├── reducers.ts │ │ │ │ ├── route.test.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── types.ts │ │ │ ├── server/ │ │ │ │ ├── actions.ts │ │ │ │ ├── index.ts │ │ │ │ ├── operations.ts │ │ │ │ ├── reducers.ts │ │ │ │ ├── server.test.ts │ │ │ │ └── types.ts │ │ │ ├── store.ts │ │ │ ├── types.ts │ │ │ └── websocket/ │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── operations.ts │ │ │ ├── reducers.ts │ │ │ ├── types.ts │ │ │ └── websocket.test.ts │ │ ├── ui/ │ │ │ ├── App.test.tsx │ │ │ ├── App.tsx │ │ │ ├── __snapshots__/ │ │ │ │ └── App.test.tsx.snap │ │ │ ├── components/ │ │ │ │ ├── Alerts.test.tsx │ │ │ │ ├── Alerts.tsx │ │ │ │ ├── Background.tsx │ │ │ │ ├── DemoDropdown.css │ │ │ │ ├── DemoDropdown.test.tsx │ │ │ │ ├── DemoDropdown.tsx │ │ │ │ ├── Location.test.tsx │ │ │ │ ├── Location.tsx │ │ │ │ ├── LocationList.css │ │ │ │ ├── LocationList.test.tsx │ │ │ │ ├── LocationList.tsx │ │ │ │ ├── LocationMarker.test.tsx │ │ │ │ ├── LocationMarker.tsx │ │ │ │ ├── RouteMap.test.tsx │ │ │ │ ├── RouteMap.tsx │ │ │ │ ├── SearchBox.test.tsx │ │ │ │ ├── SearchBox.tsx │ │ │ │ ├── Vehicle.test.tsx │ │ │ │ ├── Vehicle.tsx │ │ │ │ └── __snapshots__/ │ │ │ │ ├── Alerts.test.tsx.snap │ │ │ │ ├── DemoDropdown.test.tsx.snap │ │ │ │ ├── Location.test.tsx.snap │ │ │ │ ├── LocationList.test.tsx.snap │ │ │ │ ├── LocationMarker.test.tsx.snap │ │ │ │ ├── RouteMap.test.tsx.snap │ │ │ │ ├── SearchBox.test.tsx.snap │ │ │ │ └── Vehicle.test.tsx.snap │ │ │ ├── connection/ │ │ │ │ ├── ConnectionError.test.tsx │ │ │ │ ├── ConnectionError.tsx │ │ │ │ ├── ConnectionManager.test.tsx │ │ │ │ ├── ConnectionManager.tsx │ │ │ │ ├── __snapshots__/ │ │ │ │ │ ├── ConnectionError.test.tsx.snap │ │ │ │ │ └── ConnectionManager.test.tsx.snap │ │ │ │ └── index.ts │ │ │ ├── header/ │ │ │ │ ├── Header.test.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Navigation.test.tsx │ │ │ │ ├── Navigation.tsx │ │ │ │ └── __snapshots__/ │ │ │ │ ├── Header.test.tsx.snap │ │ │ │ └── Navigation.test.tsx.snap │ │ │ ├── pages/ │ │ │ │ ├── Demo.test.tsx │ │ │ │ ├── Demo.tsx │ │ │ │ ├── InfoBlock.test.tsx │ │ │ │ ├── InfoBlock.tsx │ │ │ │ ├── Route.test.tsx │ │ │ │ ├── Route.tsx │ │ │ │ ├── Vehicles.test.tsx │ │ │ │ ├── Vehicles.tsx │ │ │ │ ├── Visits.test.tsx │ │ │ │ ├── Visits.tsx │ │ │ │ ├── __snapshots__/ │ │ │ │ │ ├── Demo.test.tsx.snap │ │ │ │ │ ├── InfoBlock.test.tsx.snap │ │ │ │ │ ├── Route.test.tsx.snap │ │ │ │ │ ├── Vehicles.test.tsx.snap │ │ │ │ │ └── Visits.test.tsx.snap │ │ │ │ ├── common.ts │ │ │ │ └── index.ts │ │ │ └── shallow-test-util.ts │ │ └── websocket/ │ │ ├── WebSocketClient.test.ts │ │ └── WebSocketClient.ts │ └── tsconfig.json ├── optaweb-vehicle-routing-standalone/ │ ├── .gitignore │ ├── data/ │ │ └── openstreetmap/ │ │ ├── CREDITS.adoc │ │ └── planet_12.032,53.0171_12.1024,53.0491.osm.pbf │ ├── pom.xml │ └── src/ │ └── main/ │ ├── assembly/ │ │ └── assembly-quarkus-app.xml │ └── resources/ │ ├── META-INF/ │ │ └── undertow-handlers.conf │ └── application.properties ├── pom.xml ├── runLocally.sh └── runOnOpenShift.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Default to linux endings * text eol=lf # OS specific files ################### *.sh eol=lf *.bat eol=crlf # Binary files ############## # Image files *.png binary *.jpg binary *.gif binary *.bmp binary *.ico binary # Audio files *.wav binary *.mp3 binary *.ogg binary # Other binary files *.jar binary *.pdf binary *.xls binary *.xlsx binary ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: maven directory: "/" schedule: interval: daily time: '03:00' open-pull-requests-limit: 0 target-branch: "main" commit-message: prefix: "[bot][main]" - package-ecosystem: maven directory: "/" schedule: interval: daily time: '03:00' open-pull-requests-limit: 0 target-branch: "8.13.x" commit-message: prefix: "[bot][8.13.x]" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .gitignore ================================================ /.DATA_DIR_LAST /target /local # Maven Profiler reports .profiler # IntelliJ .idea *.ipr *.iws *.iml # NetBeans nbproject # STS (Spring Tools Suite) .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache # Visual Studio Code .vscode # macOS .DS_Store files .DS_Store ================================================ FILE: .mvn/extensions.xml ================================================ fr.jcgay.maven maven-profiler 3.0 io.takari.aether takari-local-repository 0.11.3 io.takari takari-filemanager 0.8.3 io.takari.maven takari-smart-builder 0.6.1 ================================================ FILE: .mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * 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. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: CONTRIBUTING.adoc ================================================ = Developing Drools, OptaPlanner and jBPM *If you want to build or contribute to a kiegroup project, https://github.com/kiegroup/droolsjbpm-build-bootstrap/blob/main/README.md[read this document].* *It will save you and us a lot of time by setting up your development environment correctly.* It solves all known pitfalls that can disrupt your development. It also describes all guidelines, tips and tricks. If you want your pull requests (or patches) to be merged into main, please respect those guidelines. ================================================ FILE: CREDITS.adoc ================================================ "link:https://www.iconfinder.com/icons/2222740/big_building_construction_home_house_icon[Big, building, construction, home, house icon]" by https://www.iconfinder.com/strokeicon[strongicon] is licensed under https://creativecommons.org/licenses/by/3.0/[CC BY 3.0] ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: README.adoc ================================================ :projectKey: org.optaweb.vehiclerouting:optaweb-vehicle-routing *This project is no longer maintained.* Visit https://github.com/kiegroup/optaplanner-quickstarts/tree/stable/use-cases/vehicle-routing[OptaPlanner Vehicle Routing Quickstart] to see how to integrate https://www.optaplanner.org/[OptaPlanner] in your application. = OptaWeb Vehicle Routing image:https://img.shields.io/badge/stackoverflow-ask_question-orange.svg?logo=stackoverflow[ "Ask question on Stack Overflow",link="https://stackoverflow.com/questions/tagged/optaplanner"] image:https://img.shields.io/badge/zulip-join_chat-brightgreen.svg?logo=zulip[ "Join Zulip Chat",link="https://kie.zulipchat.com/#narrow/stream/232679-optaplanner"] Web application for solving the https://www.optaplanner.org/learn/useCases/vehicleRoutingProblem.html[Vehicle Routing Problem] using https://www.optaplanner.org/[OptaPlanner]. == Run the application using a Bash script If you're on Linux or macOS, you can use `runLocally.sh` to start the application on your computer. * Use `runLocally.sh` with no arguments to run with the defaults. This will download an OSM file needed to work with the built-in data set. * Use `runLocally.sh -i` for the interactive mode. In this mode you can choose from downloaded OSM files or download more OSM files. * Use `runLocally.sh ` to run with the selected region. See the xref:optaweb-vehicle-routing-docs/src/main/asciidoc/run-locally.adoc[documentation] to learn more about the `runLocally.sh` script. === Getting started video The following https://youtu.be/rEeAML74oWo?t=107[video] shows how to download the OptaPlanner Vehicle Routing distribution and run it using the `runLocally.sh` script. == Development Read the <> chapter in the documentation. ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: optaweb-vehicle-routing-backend/.dockerignore ================================================ * !target/*-runner !target/*-runner.jar !target/lib/* !target/quarkus-app*/* ================================================ FILE: optaweb-vehicle-routing-backend/.env-example ================================================ APP_DEMO_DATA_SET_DIR=${user.home}/.optaweb-vehicle-routing/dataset APP_PERSISTENCE_H2_DIR=${user.home}/.optaweb-vehicle-routing/db APP_PERSISTENCE_H2_FILENAME=dev APP_ROUTING_GH_DIR=${user.home}/.optaweb-vehicle-routing/graphhopper APP_ROUTING_OSM_DIR=${user.home}/.optaweb-vehicle-routing/openstreetmap #APP_ROUTING_ENGINE=AIR #QUARKUS_HTTP_PORT=8180 #LOG_LEVEL_APP=DEBUG #LOG_LEVEL_OPTAPLANNER=DEBUG #LOG_LEVEL_HIBERNATE=DEBUG #LOG_LEVEL_DROOLS=DEBUG #LOG_LEVEL_RESTEASY=DEBUG QUARKUS_OPTAPLANNER_SOLVER_TERMINATION_SPENT_LIMIT=20s #APP_ROUTING_OSM_FILE=massachusetts-latest.osm.pbf #APP_REGION_COUNTRY_CODES=US ================================================ FILE: optaweb-vehicle-routing-backend/.gitignore ================================================ !.mvn/wrapper/maven-wrapper.jar /.env /panache-archive.marker /target /local ================================================ FILE: optaweb-vehicle-routing-backend/Dockerfile ================================================ FROM docker.io/adoptopenjdk/openjdk15:ubi-minimal-jre ENV APP_ROUTING_ENGINE air COPY target/*-exec.jar /opt/app/optaweb-vehicle-routing.jar WORKDIR /opt/app VOLUME /opt/app/local CMD ["java", "-jar", "optaweb-vehicle-routing.jar"] EXPOSE 8080 ================================================ FILE: optaweb-vehicle-routing-backend/README.adoc ================================================ = OptaWeb Vehicle Routing back end See the <<../optaweb-vehicle-routing-docs/src/main/asciidoc/development-guide#backend,back end development chapter>> in the documentation. ================================================ FILE: optaweb-vehicle-routing-backend/pom.xml ================================================ 4.0.0 org.optaweb.vehiclerouting optaweb-vehicle-routing 8.35.0.Final optaweb-vehicle-routing-backend jar OptaWeb Vehicle Routing Backend org.optaweb.vehiclerouting.backend true 11 11 UTF-8 UTF-8 com.fasterxml.jackson.dataformat jackson-dataformat-yaml io.quarkus quarkus-hibernate-orm-panache io.quarkus quarkus-jdbc-h2 com.h2database h2 true io.quarkus quarkus-jdbc-postgresql org.postgresql postgresql true org.optaplanner optaplanner-quarkus com.google.guava guava com.neovisionaries nv-i18n io.quarkus quarkus-resteasy-jackson com.graphhopper graphhopper-core jakarta.xml.bind jakarta.xml.bind-api runtime org.junit.jupiter junit-jupiter-api test org.junit.jupiter junit-jupiter-engine test org.mockito mockito-junit-jupiter test org.optaplanner optaplanner-test test io.quarkus quarkus-junit5-mockito test org.assertj assertj-core test org.apache.maven.plugins maven-compiler-plugin -Xlint:all -Xlint:-processing -Xlint:-serial io.quarkus quarkus-maven-plugin ${version.io.quarkus} true build generate-code generate-code-tests quarkus-app-h2 true com.h2database:h2::jar postgresql build postgresql quarkus-app-postgresql true org.postgresql:postgresql::jar maven-surefire-plugin org.jboss.logmanager.LogManager ${maven.home} org.apache.maven.plugins maven-javadoc-plugin true mutationCoverage org.pitest pitest-maven 1.11.4 org.pitest pitest-junit5-plugin 1.1.2 local/pit-reports true 2 DEFAULTS NON_VOID_METHOD_CALLS REMOVE_CONDITIONALS java.lang.StringBuilder org.slf4j *Config *Properties hashCode *IntegrationTest default-pitest verify mutationCoverage native native maven-failsafe-plugin integration-test verify ${project.build.directory}/${project.build.finalName}-runner org.jboss.logmanager.LogManager ${maven.home} native ================================================ FILE: optaweb-vehicle-routing-backend/src/main/docker/Dockerfile.jvm ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode # # Before building the container image run: # # ./mvnw package # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/optaweb-vehicle-routing-backend-jvm . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/optaweb-vehicle-routing-backend-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 5005 # # Then run the container using : # # docker run -i --rm -p 8080:8080 quarkus/optaweb-vehicle-routing-backend-jvm # # This image uses the `run-java.sh` script to run the application. # This scripts computes the command line to execute your Java application, and # includes memory/GC tuning. # You can configure the behavior using the following environment properties: # - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") # - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options # in JAVA_OPTS (example: "-Dsome.property=foo") # - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is # used to calculate a default maximal heap memory based on a containers restriction. # If used in a container without any memory constraints for the container then this # option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio # of the container available memory as set here. The default is `50` which means 50% # of the available memory is used as an upper boundary. You can skip this mechanism by # setting this value to `0` in which case no `-Xmx` option is added. # - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This # is used to calculate a default initial heap memory based on the maximum heap memory. # If used in a container without any memory constraints for the container then this # option has no effect. If there is a memory constraint then `-Xms` is set to a ratio # of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` # is used as the initial heap size. You can skip this mechanism by setting this value # to `0` in which case no `-Xms` option is added (example: "25") # - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. # This is used to calculate the maximum value of the initial heap memory. If used in # a container without any memory constraints for the container then this option has # no effect. If there is a memory constraint then `-Xms` is limited to the value set # here. The default is 4096MB which means the calculated value of `-Xms` never will # be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") # - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output # when things are happening. This option, if set to true, will set # `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). # - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: # true"). # - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). # - CONTAINER_CORE_LIMIT: A calculated core limit as described in # https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") # - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). # - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. # (example: "20") # - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. # (example: "40") # - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. # (example: "4") # - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus # previous GC times. (example: "90") # - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") # - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") # - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should # contain the necessary JRE command-line options to specify the required GC, which # will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). # - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") # - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") # - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be # accessed directly. (example: "foo.example.com,bar.example.com") # ### FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 ENV LANGUAGE='en_US:en' ARG QUARKUS_APP_BUILD_QUALIFIER=h2 # We make four distinct layers so if there are application changes the library layers can be re-used COPY --chown=185 target/quarkus-app-${QUARKUS_APP_BUILD_QUALIFIER}/lib/ /deployments/lib/ COPY --chown=185 target/quarkus-app-${QUARKUS_APP_BUILD_QUALIFIER}/*.jar /deployments/ COPY --chown=185 target/quarkus-app-${QUARKUS_APP_BUILD_QUALIFIER}/app/ /deployments/app/ COPY --chown=185 target/quarkus-app-${QUARKUS_APP_BUILD_QUALIFIER}/quarkus/ /deployments/quarkus/ EXPOSE 8080 USER 185 ENV AB_JOLOKIA_OFF="" ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" ENV APP_PERSISTENCE_H2_DIR=/deployments/local/db ================================================ FILE: optaweb-vehicle-routing-backend/src/main/docker/Dockerfile.legacy-jar ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode # # Before building the container image run: # # ./mvnw package -Dquarkus.package.type=legacy-jar # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/optaweb-vehicle-routing-backend-legacy-jar . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/optaweb-vehicle-routing-backend-legacy-jar # # 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 5005 # # Then run the container using : # # docker run -i --rm -p 8080:8080 quarkus/optaweb-vehicle-routing-backend-legacy-jar # # This image uses the `run-java.sh` script to run the application. # This scripts computes the command line to execute your Java application, and # includes memory/GC tuning. # You can configure the behavior using the following environment properties: # - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") # - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options # in JAVA_OPTS (example: "-Dsome.property=foo") # - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is # used to calculate a default maximal heap memory based on a containers restriction. # If used in a container without any memory constraints for the container then this # option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio # of the container available memory as set here. The default is `50` which means 50% # of the available memory is used as an upper boundary. You can skip this mechanism by # setting this value to `0` in which case no `-Xmx` option is added. # - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This # is used to calculate a default initial heap memory based on the maximum heap memory. # If used in a container without any memory constraints for the container then this # option has no effect. If there is a memory constraint then `-Xms` is set to a ratio # of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` # is used as the initial heap size. You can skip this mechanism by setting this value # to `0` in which case no `-Xms` option is added (example: "25") # - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. # This is used to calculate the maximum value of the initial heap memory. If used in # a container without any memory constraints for the container then this option has # no effect. If there is a memory constraint then `-Xms` is limited to the value set # here. The default is 4096MB which means the calculated value of `-Xms` never will # be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") # - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output # when things are happening. This option, if set to true, will set # `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). # - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: # true"). # - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). # - CONTAINER_CORE_LIMIT: A calculated core limit as described in # https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") # - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). # - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. # (example: "20") # - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. # (example: "40") # - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. # (example: "4") # - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus # previous GC times. (example: "90") # - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") # - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") # - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should # contain the necessary JRE command-line options to specify the required GC, which # will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). # - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") # - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") # - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be # accessed directly. (example: "foo.example.com,bar.example.com") # ### FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 ENV LANGUAGE='en_US:en' COPY target/lib/* /deployments/lib/ COPY target/*-runner.jar /deployments/quarkus-run.jar EXPOSE 8080 USER 185 ENV AB_JOLOKIA_OFF="" ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" ================================================ FILE: optaweb-vehicle-routing-backend/src/main/docker/Dockerfile.native ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. # # Before building the container image run: # # ./mvnw package -Pnative # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.native -t quarkus/optaweb-vehicle-routing-backend . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/optaweb-vehicle-routing-backend # ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 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"] ================================================ FILE: optaweb-vehicle-routing-backend/src/main/docker/Dockerfile.native-micro ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. # It uses a micro base image, tuned for Quarkus native executables. # It reduces the size of the resulting container image. # Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. # # Before building the container image run: # # ./mvnw package -Pnative # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/optaweb-vehicle-routing-backend . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/optaweb-vehicle-routing-backend # ### FROM quay.io/quarkus/quarkus-micro-image:1.0 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"] ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/Profiles.java ================================================ package org.optaweb.vehiclerouting; public class Profiles { public static final String TEST = "test"; private Profiles() { throw new AssertionError("Constants class"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/Coordinates.java ================================================ package org.optaweb.vehiclerouting.domain; import java.math.BigDecimal; import java.util.Objects; /** * Horizontal geographical coordinates consisting of latitude and longitude. */ public class Coordinates { private final BigDecimal latitude; private final BigDecimal longitude; public Coordinates(BigDecimal latitude, BigDecimal longitude) { this.latitude = Objects.requireNonNull(latitude); this.longitude = Objects.requireNonNull(longitude); } /** * Create coordinates with the given latitude and longitude. * * @param latitude latitude * @param longitude longitude * @return coordinates with the given latitude and longitude */ public static Coordinates of(double latitude, double longitude) { return new Coordinates(BigDecimal.valueOf(latitude), BigDecimal.valueOf(longitude)); } /** * Latitude. * * @return latitude (never {@code null}) */ public BigDecimal latitude() { return latitude; } /** * Longitude. * * @return longitude (never {@code null}) */ public BigDecimal longitude() { return longitude; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Coordinates coordinates = (Coordinates) o; return latitude.compareTo(coordinates.latitude) == 0 && longitude.compareTo(coordinates.longitude) == 0; } @Override public int hashCode() { return Objects.hash(latitude.doubleValue(), longitude.doubleValue()); } @Override public String toString() { return "[" + latitude.toPlainString() + ", " + longitude.toPlainString() + ']'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/CountryCodeValidator.java ================================================ package org.optaweb.vehiclerouting.domain; import static java.util.stream.Collectors.toList; import java.util.List; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.neovisionaries.i18n.CountryCode; /** * Validates ISO 3166-1 alpha-2 country codes. */ public class CountryCodeValidator { private static final Logger logger = LoggerFactory.getLogger(CountryCodeValidator.class); private CountryCodeValidator() { throw new AssertionError("Utility class"); } /** * Validates the list of country codes and returns a normalized copy. * * @param countryCodes input list * @return normalized copy of the input list converted to upper case and without duplicates * @throws NullPointerException if the list is {@code null} or if any of its elements is {@code null} * @throws IllegalArgumentException if any of the elements is not an ISO 3166-1 alpha-2 country code */ public static List validate(List countryCodes) { List upperCaseCountries = Objects.requireNonNull(countryCodes).stream() .map(String::toUpperCase) .collect(toList()); List invalidCodes = upperCaseCountries.stream() .filter(s -> CountryCode.getByAlpha2Code(s) == null) .collect(toList()); if (!invalidCodes.isEmpty()) { throw new IllegalArgumentException( "Following elements (" + invalidCodes + ") are not valid ISO 3166-1 alpha-2 country codes"); } List uniqueCountries = upperCaseCountries.stream().distinct().collect(toList()); if (uniqueCountries.size() < countryCodes.size()) { logger.warn("Duplicate items were removed from {}", countryCodes); } return uniqueCountries; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/Distance.java ================================================ package org.optaweb.vehiclerouting.domain; /** * Travel cost (distance between two {@link Location locations} or the length of a {@link Route route}). */ public class Distance { /** * Zero distance, for example the distance from a location to itself. */ public static final Distance ZERO = Distance.ofMillis(0); private final long millis; /** * Create a distance of the given milliseconds. * * @param millis must be positive or zero * @return distance */ public static Distance ofMillis(long millis) { return new Distance(millis); } private Distance(long millis) { if (millis < 0) { throw new IllegalArgumentException("Milliseconds (" + millis + ") must not be negative."); } this.millis = millis; } /** * Distance in milliseconds. * * @return positive number or zero */ public long millis() { return millis; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Distance distance = (Distance) o; return millis == distance.millis; } @Override public int hashCode() { return Long.hashCode(millis); } @Override public String toString() { return String.format( "%dh %dm %ds %dms", millis / 3600_000, millis / 60_000 % 60, millis / 1000 % 60, millis % 1000); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/Location.java ================================================ package org.optaweb.vehiclerouting.domain; /** * A unique location significant to the user. */ public class Location extends LocationData { private final long id; public Location(long id, Coordinates coordinates) { // TODO remove this? this(id, coordinates, ""); } public Location(long id, Coordinates coordinates, String description) { super(coordinates, description); this.id = id; } /** * Location's ID. * * @return unique ID */ public long id() { return id; } /** * Full description of the location including its ID, description and coordinates. * * @return full description */ public String fullDescription() { return "[" + id + "]: " + super.toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Location location = (Location) o; return id == location.id; } @Override public int hashCode() { return Long.hashCode(id); } @Override public String toString() { return description().isEmpty() ? Long.toString(id) : (id + ": '" + description() + "'"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/LocationData.java ================================================ package org.optaweb.vehiclerouting.domain; import java.util.Objects; /** * Location properties. It's not an entity yet (it doesn't have an identity, it's a value object). * It might be the data about a location sent from a client or data stored in a file, * ready to be loaded but not yet tied to a specific location entity. */ public class LocationData { private final Coordinates coordinates; private final String description; /** * Create location data. * * @param coordinates never {@code null} * @param description never {@code null} */ public LocationData(Coordinates coordinates, String description) { this.coordinates = Objects.requireNonNull(coordinates); this.description = Objects.requireNonNull(description); } /** * Location coordinates. * * @return coordinates (never {@code null}) */ public Coordinates coordinates() { return coordinates; } /** * Location description. * * @return description (never {@code null}) */ public String description() { return description; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } LocationData that = (LocationData) o; return coordinates.equals(that.coordinates) && description.equals(that.description); } @Override public int hashCode() { return Objects.hash(coordinates, description); } @Override public String toString() { return (description.isEmpty() ? "" : "'" + description + "'") + " " + coordinates; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/Route.java ================================================ package org.optaweb.vehiclerouting.domain; import static java.util.stream.Collectors.toList; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; /** * Vehicle's itinerary (sequence of visits) and its depot. This entity cannot exist without the vehicle and the depot * but it's allowed to have no visits when the vehicle hasn't been assigned any (it's idle). *

* This entity describes part of a {@link RoutingPlan solution} of the vehicle routing problem * (assignment of a subset of visits to one of the vehicles). * It doesn't carry the data about physical tracks between adjacent visits. * Geographical data is held by {@link RouteWithTrack}. */ public class Route { private final Vehicle vehicle; private final Location depot; private final List visits; /** * Create a vehicle route. * * @param vehicle the vehicle assigned to this route (not {@code null}) * @param depot vehicle's depot (not {@code null}) * @param visits list of visits (not {@code null}) */ public Route(Vehicle vehicle, Location depot, List visits) { this.vehicle = Objects.requireNonNull(vehicle); this.depot = Objects.requireNonNull(depot); this.visits = new ArrayList<>(Objects.requireNonNull(visits)); // TODO Probably remove this check when we have more types: new Route(Depot depot, List visits). // Then visits obviously cannot contain the depot. But will we still require that no visit has the same // location as the depot? (I don't think so). if (visits.contains(depot)) { throw new IllegalArgumentException("Depot (" + depot + ") must not be one of the visits (" + visits + ")"); } long uniqueVisits = visits.stream().distinct().count(); if (uniqueVisits < visits.size()) { long duplicates = visits.size() - uniqueVisits; throw new IllegalArgumentException("Some visits have been visited multiple times (" + duplicates + ")"); } } /** * The vehicle assigned to this route. * * @return route's vehicle (never {@code null}) */ public Vehicle vehicle() { return vehicle; } /** * Depot in which the route starts and ends. * * @return route's depot (never {@code null}) */ public Location depot() { return depot; } /** * List of vehicle's visits (not including the depot). * * @return list of visits */ public List visits() { return Collections.unmodifiableList(visits); } @Override public String toString() { return "Route{" + "vehicle=" + vehicle + ", depot=" + depot.id() + ", visits=" + visits.stream().map(Location::id).collect(toList()) + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/RouteWithTrack.java ================================================ package org.optaweb.vehiclerouting.domain; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; /** * Vehicle's {@link Route itinerary} enriched with detailed geographical description of the route. * This object contains data needed to visualize vehicle's route on a map. */ public class RouteWithTrack extends Route { private final List> track; /** * Create a route with track. When route is empty (no visits), track must be empty too and vice-versa * (non-empty route must have a non-empty track). * * @param route vehicle's route (not {@code null}) * @param track track going through all visits (not {@code null}) */ public RouteWithTrack(Route route, List> track) { super(route.vehicle(), route.depot(), route.visits()); this.track = new ArrayList<>(Objects.requireNonNull(track)); if (route.visits().isEmpty() && !track.isEmpty() || !route.visits().isEmpty() && track.isEmpty()) { throw new IllegalArgumentException("Route and track must be either both empty or both non-empty"); } } /** * Vehicle's track that goes from vehicle's depot through all visits and returns to the depot. * * @return vehicle's track (not {@code null}) */ public List> track() { return Collections.unmodifiableList(track); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/RoutingPlan.java ================================================ package org.optaweb.vehiclerouting.domain; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Route plan for the whole vehicle fleet. */ public class RoutingPlan { private static final Logger logger = LoggerFactory.getLogger(RoutingPlan.class); private final Distance distance; private final List vehicles; private final Location depot; private final List visits; private final List routes; /** * Create a routing plan. * * @param distance the overall travel distance * @param vehicles all available vehicles * @param depot the depot (may be {@code null}) * @param visits all visits * @param routes routes of all vehicles */ public RoutingPlan( Distance distance, List vehicles, Location depot, List visits, List routes) { this.distance = Objects.requireNonNull(distance); this.vehicles = new ArrayList<>(Objects.requireNonNull(vehicles)); this.depot = depot; this.visits = new ArrayList<>(Objects.requireNonNull(visits)); this.routes = new ArrayList<>(Objects.requireNonNull(routes)); if (depot == null) { if (!routes.isEmpty()) { throw new IllegalArgumentException("Routes must be empty when depot is null"); } } else if (routes.size() != vehicles.size()) { throw new IllegalArgumentException(describeVehiclesRoutesInconsistency( "There must be exactly one route per vehicle", vehicles, routes)); } else if (haveDifferentVehicles(vehicles, routes)) { throw new IllegalArgumentException(describeVehiclesRoutesInconsistency( "Some routes are assigned to non-existent vehicles", vehicles, routes)); } else if (!routes.isEmpty()) { List visited = routes.stream() .map(Route::visits) .flatMap(Collection::stream) .collect(toList()); ArrayList unvisited = new ArrayList<>(visits); unvisited.removeAll(visited); if (!unvisited.isEmpty()) { // This happens because we're also publishing solutions that are not fully initialized. // TODO decide whether this allowed or not logger.warn("Some visits are unvisited: {}", unvisited); } visited.removeAll(visits); if (!visited.isEmpty()) { throw new IllegalArgumentException( "Some routes are going through visits that haven't been defined: " + visited); } } } private static boolean haveDifferentVehicles(List vehicles, List routes) { return routes.stream() .map(Route::vehicle) .anyMatch(vehicle -> !vehicles.contains(vehicle)); } private static String describeVehiclesRoutesInconsistency( String cause, List vehicles, List routes) { List vehicleIdsFromRoutes = routes.stream() .map(route -> route.vehicle().id()) .collect(toList()); return cause + ":\n- Vehicles (" + vehicles.size() + "): " + vehicles + "\n- Routes' vehicleIds (" + routes.size() + "): " + vehicleIdsFromRoutes; } /** * Create an empty routing plan. * * @return empty routing plan */ public static RoutingPlan empty() { return new RoutingPlan(Distance.ZERO, emptyList(), null, emptyList(), emptyList()); } /** * Total distance traveled (sum of distances of all routes). * * @return travel distance */ public Distance distance() { return distance; } /** * All available vehicles. * * @return all vehicles */ public List vehicles() { return Collections.unmodifiableList(vehicles); } /** * Routes of all vehicles in the depot. Includes empty routes of vehicles that stay in the depot. * * @return all routes (may be empty when there is no depot or no vehicles) */ public List routes() { return Collections.unmodifiableList(routes); } /** * All visits that are part of the routing problem. * * @return all visits */ public List visits() { return Collections.unmodifiableList(visits); } /** * The depot. * * @return depot (may be missing) */ public Optional depot() { return Optional.ofNullable(depot); } /** * Routing plan is empty when there is no depot, no vehicles and no routes. * * @return {@code true} if the plan is empty */ public boolean isEmpty() { // No need to check routes. No depot => no routes. return depot == null && vehicles.isEmpty(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/RoutingProblem.java ================================================ package org.optaweb.vehiclerouting.domain; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; /** * Definition of the vehicle routing problem instance. */ public class RoutingProblem { private final String name; private final List vehicles; private final LocationData depot; private final List visits; /** * Create routing problem instance. * * @param name the instance name * @param vehicles list of vehicles (not {@code null}) * @param depot the depot (may be {@code null} if there is no depot) * @param visits list of visits (not {@code null}) */ public RoutingProblem( String name, List vehicles, LocationData depot, List visits) { this.name = Objects.requireNonNull(name); this.vehicles = new ArrayList<>(Objects.requireNonNull(vehicles)); this.depot = depot; this.visits = new ArrayList<>(Objects.requireNonNull(visits)); } /** * Get routing problem instance name. * * @return routing problem instance name */ public String name() { return name; } /** * Get the depot. * * @return depot (never {@code null}) */ public Optional depot() { return Optional.ofNullable(depot); } /** * Get locations that should be visited. * * @return visits */ public List visits() { return visits; } /** * Vehicles that are part of the problem definition. * * @return vehicles */ public List vehicles() { return vehicles; } @Override public String toString() { return name; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/Vehicle.java ================================================ package org.optaweb.vehiclerouting.domain; /** * Vehicle that can be used to deliver cargo to visits. */ public class Vehicle extends VehicleData { private final long id; Vehicle(long id, String name, int capacity) { super(name, capacity); this.id = id; } /** * Vehicle's ID. * * @return unique ID */ public long id() { return id; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Vehicle vehicle = (Vehicle) o; return id == vehicle.id; } @Override public int hashCode() { return Long.hashCode(id); } @Override public String toString() { return name().isEmpty() ? Long.toString(id) : (id + ": '" + name() + "'"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/VehicleData.java ================================================ package org.optaweb.vehiclerouting.domain; import java.util.Objects; /** * Data about a vehicle. */ public class VehicleData { private final String name; private final int capacity; VehicleData(String name, int capacity) { this.name = Objects.requireNonNull(name); this.capacity = capacity; } /** * Vehicle's name (unique description). * * @return vehicle's name */ public String name() { return name; } /** * Vehicle's capacity. * * @return vehicle's capacity */ public int capacity() { return capacity; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } VehicleData that = (VehicleData) o; return capacity == that.capacity && name.equals(that.name); } @Override public int hashCode() { return Objects.hash(name, capacity); } @Override public String toString() { return name.isEmpty() ? "" : "'" + name + "'"; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/VehicleFactory.java ================================================ package org.optaweb.vehiclerouting.domain; /** * Creates {@link Vehicle} instances. */ public class VehicleFactory { private VehicleFactory() { throw new AssertionError("Utility class"); } /** * Create vehicle data. * * @param name vehicle's name * @param capacity vehicle's capacity * @return vehicle data */ public static VehicleData vehicleData(String name, int capacity) { return new VehicleData(name, capacity); } /** * Create a new vehicle with the given ID, name and capacity. * * @param id vehicle's ID * @param name vehicle's name * @param capacity vehicle's capacity * @return new vehicle */ public static Vehicle createVehicle(long id, String name, int capacity) { return new Vehicle(id, name, capacity); } /** * Create a vehicle with given ID and capacity of zero. The vehicle will have a non-empty name. * * @param id vehicle's ID * @return new testing vehicle instance */ public static Vehicle testVehicle(long id) { return new Vehicle(id, "Vehicle " + id, 0); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/domain/package-info.java ================================================ /** * Domain model. Contains vehicles, depots, visits and so on. * The code in this package only depends on {@link java.lang} and is not affected * by any framework used in this project. */ package org.optaweb.vehiclerouting.domain; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceCrudRepository.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import javax.enterprise.context.ApplicationScoped; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.panache.common.Parameters; /** * Distance repository. */ @ApplicationScoped public class DistanceCrudRepository implements PanacheRepositoryBase { void deleteByFromIdOrToId(long deletedLocationId) { delete( "fromId = :deletedLocationId or toId = :deletedLocationId", Parameters.with("deletedLocationId", deletedLocationId)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceEntity.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import java.util.Objects; import javax.persistence.EmbeddedId; import javax.persistence.Entity; /** * Distance between two locations that can be persisted. */ @Entity class DistanceEntity { @EmbeddedId private DistanceKey key; private Long distance; protected DistanceEntity() { // for JPA } DistanceEntity(DistanceKey key, Long distance) { this.key = Objects.requireNonNull(key); this.distance = Objects.requireNonNull(distance); } DistanceKey getKey() { return key; } Long getDistance() { return distance; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DistanceEntity that = (DistanceEntity) o; return key.equals(that.key) && distance.equals(that.distance); } @Override public int hashCode() { return Objects.hash(key, distance); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceKey.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import java.io.Serializable; import java.util.Objects; import javax.persistence.Embeddable; /** * Composite key for {@link DistanceEntity}. */ @Embeddable class DistanceKey implements Serializable { // TODO make it a foreign key to LocationEntity private Long fromId; private Long toId; protected DistanceKey() { // for JPA } DistanceKey(long fromId, long toId) { this.fromId = fromId; this.toId = toId; } Long getFromId() { return fromId; } void setFromId(Long fromId) { this.fromId = fromId; } Long getToId() { return toId; } void setToId(Long toId) { this.toId = toId; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DistanceKey that = (DistanceKey) o; return fromId.equals(that.fromId) && toId.equals(that.toId); } @Override public int hashCode() { return Objects.hash(fromId, toId); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceRepositoryImpl.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import java.util.Optional; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.service.distance.DistanceRepository; @ApplicationScoped class DistanceRepositoryImpl implements DistanceRepository { private final DistanceCrudRepository distanceRepository; @Inject DistanceRepositoryImpl(DistanceCrudRepository distanceRepository) { this.distanceRepository = distanceRepository; } @Override public void saveDistance(Location from, Location to, Distance distance) { DistanceEntity distanceEntity = new DistanceEntity(new DistanceKey(from.id(), to.id()), distance.millis()); distanceRepository.persist(distanceEntity); } @Override public Optional getDistance(Location from, Location to) { return distanceRepository.findByIdOptional(new DistanceKey(from.id(), to.id())) .map(DistanceEntity::getDistance) .map(Distance::ofMillis); } @Override public void deleteDistances(Location location) { distanceRepository.deleteByFromIdOrToId(location.id()); } @Override public void deleteAll() { distanceRepository.deleteAll(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/LocationCrudRepository.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import javax.enterprise.context.ApplicationScoped; import io.quarkus.hibernate.orm.panache.PanacheRepository; /** * Location repository. */ @ApplicationScoped public class LocationCrudRepository implements PanacheRepository { } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/LocationEntity.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import java.math.BigDecimal; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; /** * Persistable location. */ @Entity class LocationEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; // https://wiki.openstreetmap.org/wiki/Node#Structure @Column(precision = 9, scale = 7) private BigDecimal latitude; @Column(precision = 10, scale = 7) private BigDecimal longitude; private String description; protected LocationEntity() { // for JPA } LocationEntity(long id, BigDecimal latitude, BigDecimal longitude, String description) { this.id = id; this.latitude = Objects.requireNonNull(latitude); this.longitude = Objects.requireNonNull(longitude); this.description = Objects.requireNonNull(description); } long getId() { return id; } BigDecimal getLatitude() { return latitude; } BigDecimal getLongitude() { return longitude; } String getDescription() { return description; } @Override public String toString() { return "LocationEntity{" + "id=" + id + ", latitude=" + latitude + ", longitude=" + longitude + ", description='" + description + '\'' + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/LocationRepositoryImpl.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static java.util.stream.Collectors.toList; import java.util.List; import java.util.Optional; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.service.location.LocationRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ApplicationScoped class LocationRepositoryImpl implements LocationRepository { private static final Logger logger = LoggerFactory.getLogger(LocationRepositoryImpl.class); private final LocationCrudRepository repository; @Inject LocationRepositoryImpl(LocationCrudRepository repository) { this.repository = repository; } @Override public Location createLocation(Coordinates coordinates, String description) { LocationEntity locationEntity = new LocationEntity(0, coordinates.latitude(), coordinates.longitude(), description); repository.persist(locationEntity); Location location = toDomain(locationEntity); logger.info("Created location {}.", location.fullDescription()); return location; } @Override public List locations() { return repository.streamAll() .map(LocationRepositoryImpl::toDomain) .collect(toList()); } @Override public Location removeLocation(long id) { Optional maybeLocation = repository.findByIdOptional(id); maybeLocation.ifPresent(locationEntity -> repository.deleteById(id)); LocationEntity locationEntity = maybeLocation.orElseThrow( () -> new IllegalArgumentException("Location{id=" + id + "} doesn't exist")); Location location = toDomain(locationEntity); logger.info("Deleted location {}.", location.fullDescription()); return location; } @Override public void removeAll() { repository.deleteAll(); } @Override public Optional find(long locationId) { return repository.findByIdOptional(locationId).map(LocationRepositoryImpl::toDomain); } private static Location toDomain(LocationEntity locationEntity) { return new Location( locationEntity.getId(), new Coordinates(locationEntity.getLatitude(), locationEntity.getLongitude()), locationEntity.getDescription()); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleCrudRepository.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import javax.enterprise.context.ApplicationScoped; import io.quarkus.hibernate.orm.panache.PanacheRepository; /** * Vehicle repository. */ @ApplicationScoped public class VehicleCrudRepository implements PanacheRepository { } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleEntity.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; /** * Persistable vehicle. */ @Entity public class VehicleEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; private int capacity; protected VehicleEntity() { // for JPA } public VehicleEntity(long id, String name, int capacity) { this.id = id; this.name = name; this.capacity = capacity; } public long getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getCapacity() { return capacity; } public void setCapacity(int capacity) { this.capacity = capacity; } @Override public String toString() { return "VehicleEntity{" + "id=" + id + ", name='" + name + '\'' + ", capacity=" + capacity + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleRepositoryImpl.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static java.util.stream.Collectors.toList; import java.util.List; import java.util.Optional; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleData; import org.optaweb.vehiclerouting.domain.VehicleFactory; import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ApplicationScoped public class VehicleRepositoryImpl implements VehicleRepository { private static final Logger logger = LoggerFactory.getLogger(VehicleRepositoryImpl.class); private final VehicleCrudRepository repository; @Inject public VehicleRepositoryImpl(VehicleCrudRepository repository) { this.repository = repository; } @Override public Vehicle createVehicle(int capacity) { VehicleEntity vehicleEntity = new VehicleEntity(0, null, capacity); repository.persist(vehicleEntity); vehicleEntity.setName("Vehicle " + vehicleEntity.getId()); Vehicle vehicle = toDomain(vehicleEntity); logger.info("Created vehicle {}.", vehicle); return vehicle; } @Override public Vehicle createVehicle(VehicleData vehicleData) { VehicleEntity vehicleEntity = new VehicleEntity(0, vehicleData.name(), vehicleData.capacity()); repository.persist(vehicleEntity); Vehicle vehicle = toDomain(vehicleEntity); logger.info("Created vehicle {}.", vehicle); return vehicle; } @Override public List vehicles() { return repository.streamAll() .map(VehicleRepositoryImpl::toDomain) .collect(toList()); } @Override public Vehicle removeVehicle(long id) { Optional optionalVehicleEntity = repository.findByIdOptional(id); VehicleEntity vehicleEntity = optionalVehicleEntity.orElseThrow( () -> new IllegalArgumentException("Vehicle{id=" + id + "} doesn't exist")); repository.deleteById(id); Vehicle vehicle = toDomain(vehicleEntity); logger.info("Deleted vehicle {}.", vehicle); return vehicle; } @Override public void removeAll() { repository.deleteAll(); } @Override public Optional find(long vehicleId) { return repository.findByIdOptional(vehicleId).map(VehicleRepositoryImpl::toDomain); } @Override public Vehicle changeCapacity(long vehicleId, int capacity) { VehicleEntity vehicleEntity = repository.findByIdOptional(vehicleId).orElseThrow(() -> new IllegalArgumentException( "Can't change Vehicle{id=" + vehicleId + "} because it doesn't exist")); vehicleEntity.setCapacity(capacity); repository.flush(); return VehicleFactory.createVehicle(vehicleEntity.getId(), vehicleEntity.getName(), vehicleEntity.getCapacity()); } private static Vehicle toDomain(VehicleEntity vehicleEntity) { return VehicleFactory.createVehicle( vehicleEntity.getId(), vehicleEntity.getName(), vehicleEntity.getCapacity()); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/persistence/package-info.java ================================================ /** * Persistence infrastructure. */ package org.optaweb.vehiclerouting.plugin.persistence; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/Constants.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; public class Constants { public static final String SOLVER_CONFIG = "solverConfig.xml"; private Constants() { throw new AssertionError("Constants class"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/DistanceMapImpl.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import java.util.Objects; import org.optaweb.vehiclerouting.plugin.planner.domain.DistanceMap; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation; import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow; /** * Provides distances to {@link PlanningLocation}s by reading from a {@link DistanceMatrixRow}. */ public class DistanceMapImpl implements DistanceMap { private final DistanceMatrixRow distanceMatrixRow; public DistanceMapImpl(DistanceMatrixRow distanceMatrixRow) { this.distanceMatrixRow = Objects.requireNonNull(distanceMatrixRow); } @Override public long distanceTo(PlanningLocation location) { return distanceMatrixRow.distanceTo(location.getId()).millis(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/RouteChangedEventPublisher.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static java.util.stream.Collectors.toList; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.optaweb.vehiclerouting.service.route.RouteChangedEvent; import org.optaweb.vehiclerouting.service.route.ShallowRoute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Converts planning solution to a {@link RouteChangedEvent} and publishes it so that it can be processed by other * components that listen for this type of event. */ @ApplicationScoped class RouteChangedEventPublisher { private static final Logger logger = LoggerFactory.getLogger(RouteChangedEventPublisher.class); private final Event eventPublisher; @Inject RouteChangedEventPublisher(Event eventPublisher) { this.eventPublisher = eventPublisher; } /** * Publish solution as a {@link RouteChangedEvent}. * * @param solution solution */ void publishSolution(VehicleRoutingSolution solution) { RouteChangedEvent event = solutionToEvent(solution, this); logger.info( "New solution with {} depots, {} vehicles, {} visits, distance: {}, score: {}", solution.getDepotList().size(), solution.getVehicleList().size(), solution.getVisitList().size(), event.distance(), solution.getScore()); logger.debug("Routes: {}", event.routes()); eventPublisher.fire(event); } /** * Convert a planning domain solution to an event that can be published. * * @param solution solution * @param source source of the event * @return new event describing the solution */ static RouteChangedEvent solutionToEvent(VehicleRoutingSolution solution, Object source) { List routes = routes(solution); return new RouteChangedEvent( source, // Turn negative soft score into a positive amount of time. Distance.ofMillis(-solution.getScore().softScore()), vehicleIds(solution), depotId(solution), visitIds(solution), routes); } private static List visitIds(VehicleRoutingSolution solution) { return solution.getVisitList().stream() .map(visit -> visit.getLocation().getId()) .collect(toList()); } /** * Extract routes from the solution. Includes empty routes of vehicles that stay in the depot. * * @param solution solution * @return one route per vehicle */ private static List routes(VehicleRoutingSolution solution) { // TODO include unconnected customers in the result if (solution.getDepotList().isEmpty()) { return Collections.emptyList(); } ArrayList routes = new ArrayList<>(); for (PlanningVehicle vehicle : solution.getVehicleList()) { PlanningDepot depot = vehicle.getDepot(); if (depot == null) { throw new IllegalArgumentException( "Vehicle (id=" + vehicle.getId() + ") is not in the depot. That's not allowed"); } List visits = new ArrayList<>(); for (PlanningVisit visit : vehicle.getFutureVisits()) { if (!solution.getVisitList().contains(visit)) { throw new IllegalArgumentException("Visit (" + visit + ") doesn't exist"); } visits.add(visit.getLocation().getId()); } routes.add(new ShallowRoute(vehicle.getId(), depot.getId(), visits)); } return routes; } /** * Get IDs of vehicles in the solution. * * @param solution the solution * @return vehicle IDs */ private static List vehicleIds(VehicleRoutingSolution solution) { return solution.getVehicleList().stream() .map(PlanningVehicle::getId) .collect(toList()); } /** * Get solution's depot ID. * * @param solution the solution in which to look for the depot * @return first depot ID from the solution or {@code null} if there are no depots */ private static Long depotId(VehicleRoutingSolution solution) { return solution.getDepotList().isEmpty() ? null : solution.getDepotList().get(0).getId(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/RouteOptimizerConfig.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; /** * Configuration bean that creates {@link RouteOptimizerImpl route optimizer}'s dependencies. */ @Dependent class RouteOptimizerConfig { private final SolverFactory solverFactory; RouteOptimizerConfig(SolverFactory solverFactory) { this.solverFactory = solverFactory; } @Produces Solver solver() { return solverFactory.buildSolver(); } @Produces ListeningExecutorService executor() { ExecutorService executorService = Executors.newFixedThreadPool(1); return MoreExecutors.listeningDecorator(executorService); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/RouteOptimizerImpl.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import java.util.ArrayList; import java.util.List; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow; import org.optaweb.vehiclerouting.service.location.LocationPlanner; import org.optaweb.vehiclerouting.service.vehicle.VehiclePlanner; /** * Accumulates vehicles, depots and visits until there's enough data to start the optimization. * Solutions are published even if solving hasn't started yet due to missing facts (e.g. no vehicles or no visits). * Stops solver when vehicles or visits are reduced to zero. */ @ApplicationScoped class RouteOptimizerImpl implements LocationPlanner, VehiclePlanner { private final SolverManager solverManager; private final RouteChangedEventPublisher routeChangedEventPublisher; private final List vehicles = new ArrayList<>(); private final List visits = new ArrayList<>(); private PlanningDepot depot; @Inject RouteOptimizerImpl(SolverManager solverManager, RouteChangedEventPublisher routeChangedEventPublisher) { this.solverManager = solverManager; this.routeChangedEventPublisher = routeChangedEventPublisher; } @Override public void addLocation(Location domainLocation, DistanceMatrixRow distanceMatrixRow) { PlanningLocation location = PlanningLocationFactory.fromDomain( domainLocation, new DistanceMapImpl(distanceMatrixRow)); // Unfortunately can't start solver with an empty solution (see https://issues.redhat.com/browse/PLANNER-776) if (depot == null) { depot = new PlanningDepot(location); publishSolution(); } else { PlanningVisit visit = PlanningVisitFactory.fromLocation(location); visits.add(visit); if (vehicles.isEmpty()) { publishSolution(); } else if (visits.size() == 1) { solverManager.startSolver(SolutionFactory.solutionFromVisits(vehicles, depot, visits)); } else { solverManager.addVisit(visit); } } } @Override public void removeLocation(Location domainLocation) { if (visits.isEmpty()) { if (depot == null) { throw new IllegalArgumentException( "Cannot remove " + domainLocation + " because there are no locations"); } if (depot.getId() != domainLocation.id()) { throw new IllegalArgumentException("Cannot remove " + domainLocation + " because it doesn't exist"); } depot = null; publishSolution(); } else { if (depot.getId() == domainLocation.id()) { throw new IllegalStateException("You can only remove depot if there are no visits"); } if (!visits.removeIf(item -> item.getId() == domainLocation.id())) { throw new IllegalArgumentException("Cannot remove " + domainLocation + " because it doesn't exist"); } if (vehicles.isEmpty()) { // solver is not running publishSolution(); } else if (visits.isEmpty()) { // solver is running solverManager.stopSolver(); publishSolution(); } else { // TODO maybe allow removing location by ID (only require the necessary information) solverManager.removeVisit( PlanningVisitFactory.fromLocation(PlanningLocationFactory.fromDomain(domainLocation))); } } } @Override public void addVehicle(Vehicle domainVehicle) { PlanningVehicle vehicle = PlanningVehicleFactory.fromDomain(domainVehicle); vehicle.setDepot(depot); vehicles.add(vehicle); if (visits.isEmpty()) { publishSolution(); } else if (vehicles.size() == 1) { solverManager.startSolver(SolutionFactory.solutionFromVisits(vehicles, depot, visits)); } else { solverManager.addVehicle(vehicle); } } @Override public void removeVehicle(Vehicle domainVehicle) { if (!vehicles.removeIf(vehicle -> vehicle.getId() == domainVehicle.id())) { throw new IllegalArgumentException("Cannot remove " + domainVehicle + " because it doesn't exist"); } if (visits.isEmpty()) { // solver is not running publishSolution(); } else if (vehicles.isEmpty()) { // solver is running solverManager.stopSolver(); publishSolution(); } else { solverManager.removeVehicle(PlanningVehicleFactory.fromDomain(domainVehicle)); } } @Override public void changeCapacity(Vehicle domainVehicle) { PlanningVehicle vehicle = vehicles.stream() .filter(item -> item.getId() == domainVehicle.id()) .findFirst() .orElseThrow(() -> new IllegalArgumentException( "Cannot change capacity of " + domainVehicle + " because it doesn't exist")); vehicle.setCapacity(domainVehicle.capacity()); if (!visits.isEmpty()) { solverManager.changeCapacity(vehicle); } else { publishSolution(); } } @Override public void removeAllLocations() { solverManager.stopSolver(); depot = null; visits.clear(); publishSolution(); } @Override public void removeAllVehicles() { solverManager.stopSolver(); vehicles.clear(); publishSolution(); } private void publishSolution() { routeChangedEventPublisher.publishSolution(SolutionFactory.solutionFromVisits(vehicles, depot, visits)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/SolverManager.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.enterprise.inject.Default; import javax.inject.Inject; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent; import org.optaplanner.core.api.solver.event.SolverEventListener; import org.optaweb.vehiclerouting.plugin.planner.change.AddVehicle; import org.optaweb.vehiclerouting.plugin.planner.change.AddVisit; import org.optaweb.vehiclerouting.plugin.planner.change.ChangeVehicleCapacity; import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVehicle; import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.optaweb.vehiclerouting.service.error.ErrorEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; /** * Manages a solver running in a different thread. *

* Does following: *

    *
  • Starts solver by running {@link Solver#solve(Object problem)} in a thread that's not the caller's thread.
  • *
  • Stops the solver (synchronously).
  • *
  • Adds problem fact changes to the solver.
  • *
  • Propagates any exception that happens in {@code Solver.solver()} (in a different thread) to the thread that * interacts with {@code SolverManager}.
  • *
  • Listens for best solution changes and publishes new best solutions via {@link RouteChangedEventPublisher}.
  • *
*/ @ApplicationScoped @Default class SolverManager implements SolverEventListener { private static final Logger logger = LoggerFactory.getLogger(SolverManager.class); private final Solver solver; private final ListeningExecutorService executor; private final RouteChangedEventPublisher routeChangedEventPublisher; private final Event errorEvent; private ListenableFuture solverFuture; @Inject SolverManager( Solver solver, ListeningExecutorService executor, RouteChangedEventPublisher routeChangedEventPublisher, Event errorEvent) { this.solver = solver; this.executor = executor; this.routeChangedEventPublisher = routeChangedEventPublisher; this.errorEvent = errorEvent; this.solver.addEventListener(this); } @Override public void bestSolutionChanged(BestSolutionChangedEvent bestSolutionChangedEvent) { // CAUTION! This runs on the solver thread. Implications: // 1. The method should be as quick as possible to avoid blocking solver unnecessarily. // 2. This place is a potential source of race conditions. if (!bestSolutionChangedEvent.isEveryProblemChangeProcessed()) { logger.info("Ignoring a new best solution that has some problem facts missing"); return; } // TODO Race condition, if a servlet thread deletes that location in the middle of this method happening // on the solver thread. Make sure that location is still in the repository. // Maybe repair the solution OR ignore if it's inconsistent (log a WARNING). routeChangedEventPublisher.publishSolution(bestSolutionChangedEvent.getNewBestSolution()); // TODO @Async } void startSolver(VehicleRoutingSolution solution) { if (solverFuture != null) { throw new IllegalStateException("Solver start has already been requested"); } solverFuture = executor.submit((SolvingTask) () -> solver.solve(solution)); solverFuture.addListener( // IMPORTANT: This is happening on the solver thread. // TODO maybe restart or somehow recover? () -> { if (!solver.isTerminateEarly()) { // Solver in daemon mode can't return from solve() unless it has been terminated early // (see #stopSolver()). // So this case is only possible when an exception is thrown during solver.solve(). try { solverFuture.get(); logger.error("The solver has stopped without being terminated early so at this point" + " it is expected to have crashed but there was no exception.\n" + "If you see this other than during test execution it is probably a bug."); errorEvent.fire(new ErrorEvent( this, "Solver stopped without being terminated early and without throwing an exception." + " This is a bug.")); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while retrieving the cause of solver failure", e); } catch (ExecutionException e) { logger.error("Solver failed", e); errorEvent.fire(new ErrorEvent(this, e.toString())); } } }, MoreExecutors.directExecutor()); } void stopSolver() { if (solverFuture != null) { // TODO what happens if solver hasn't started yet (solve() is called asynchronously) solver.terminateEarly(); // make sure solver has terminated and propagate exceptions try { solverFuture.get(); solverFuture = null; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Failed to stop solver", e); } catch (ExecutionException e) { // Skipping the wrapper ExecutionException because it only tells that the problem occurred // in solverFuture.get() but that's obvious. throw new RuntimeException("Failed to stop solver", e.getCause()); } } } private void assertSolverIsAlive() { if (solverFuture == null) { throw new IllegalStateException("Solver has not started yet"); } if (solverFuture.isDone()) { try { solverFuture.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Solver has died", e); } catch (ExecutionException e) { // Skipping the wrapper ExecutionException because it only tells that the problem occurred // in solverFuture.get() but that's obvious. throw new RuntimeException("Solver has died", e.getCause()); } throw new IllegalStateException("Solver has finished solving even though it operates in daemon mode"); } } void addVisit(PlanningVisit visit) { assertSolverIsAlive(); solver.addProblemChange(new AddVisit(visit)); } void removeVisit(PlanningVisit visit) { assertSolverIsAlive(); solver.addProblemChange(new RemoveVisit(visit)); } void addVehicle(PlanningVehicle vehicle) { assertSolverIsAlive(); solver.addProblemChange(new AddVehicle(vehicle)); } void removeVehicle(PlanningVehicle vehicle) { assertSolverIsAlive(); solver.addProblemChange(new RemoveVehicle(vehicle)); } void changeCapacity(PlanningVehicle vehicle) { assertSolverIsAlive(); solver.addProblemChange(new ChangeVehicleCapacity(vehicle)); } /** * An alias interface that fixates the Callable's type parameter. This avoids unchecked warnings in tests. */ interface SolvingTask extends Callable { } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/VehicleRoutingConstraintProvider.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static org.optaplanner.core.api.score.stream.ConstraintCollectors.sum; import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; import org.optaplanner.core.api.score.stream.Constraint; import org.optaplanner.core.api.score.stream.ConstraintFactory; import org.optaplanner.core.api.score.stream.ConstraintProvider; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; public class VehicleRoutingConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { vehicleCapacity(constraintFactory), distanceFromPreviousStandstill(constraintFactory), distanceFromLastVisitToDepot(constraintFactory) }; } Constraint vehicleCapacity(ConstraintFactory constraintFactory) { return constraintFactory.forEach(PlanningVisit.class) .groupBy(PlanningVisit::getVehicle, sum(PlanningVisit::getDemand)) .filter((vehicle, demand) -> demand > vehicle.getCapacity()) .penalizeLong(HardSoftLongScore.ONE_HARD, (vehicle, demand) -> demand - vehicle.getCapacity()) .asConstraint("vehicle capacity"); } Constraint distanceFromPreviousStandstill(ConstraintFactory constraintFactory) { return constraintFactory.forEach(PlanningVisit.class) .penalizeLong(HardSoftLongScore.ONE_SOFT, PlanningVisit::distanceFromPreviousStandstill) .asConstraint("distance from previous standstill"); } Constraint distanceFromLastVisitToDepot(ConstraintFactory constraintFactory) { return constraintFactory.forEach(PlanningVisit.class) .filter(PlanningVisit::isLast) .penalizeLong(HardSoftLongScore.ONE_SOFT, PlanningVisit::distanceToDepot) .asConstraint("distance from last visit to depot"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/AddVehicle.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import java.util.Objects; import org.optaplanner.core.api.solver.change.ProblemChange; import org.optaplanner.core.api.solver.change.ProblemChangeDirector; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; public class AddVehicle implements ProblemChange { private final PlanningVehicle vehicle; public AddVehicle(PlanningVehicle vehicle) { this.vehicle = Objects.requireNonNull(vehicle); } @Override public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) { problemChangeDirector.addProblemFact(vehicle, workingSolution.getVehicleList()::add); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/AddVisit.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import java.util.Objects; import org.optaplanner.core.api.solver.change.ProblemChange; import org.optaplanner.core.api.solver.change.ProblemChangeDirector; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; public class AddVisit implements ProblemChange { private final PlanningVisit visit; public AddVisit(PlanningVisit visit) { this.visit = Objects.requireNonNull(visit); } @Override public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) { problemChangeDirector.addEntity(visit, workingSolution.getVisitList()::add); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/ChangeVehicleCapacity.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import java.util.Objects; import org.optaplanner.core.api.solver.change.ProblemChange; import org.optaplanner.core.api.solver.change.ProblemChangeDirector; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; public class ChangeVehicleCapacity implements ProblemChange { private final PlanningVehicle vehicle; public ChangeVehicleCapacity(PlanningVehicle vehicle) { this.vehicle = Objects.requireNonNull(vehicle); } @Override public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) { // No need to clone the workingVehicle because it is a planning entity, so it is already planning-cloned. // To learn more about problem fact changes, see: // https://www.optaplanner.org/docs/optaplanner/latest/repeated-planning/repeated-planning.html#problemChangeExample problemChangeDirector.changeProblemProperty(vehicle, workingVehicle -> workingVehicle.setCapacity(vehicle.getCapacity())); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/RemoveVehicle.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import java.util.Objects; import org.optaplanner.core.api.solver.change.ProblemChange; import org.optaplanner.core.api.solver.change.ProblemChangeDirector; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; public class RemoveVehicle implements ProblemChange { private final PlanningVehicle removedVehicle; public RemoveVehicle(PlanningVehicle removedVehicle) { this.removedVehicle = Objects.requireNonNull(removedVehicle); } @Override public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) { // Look up a working copy of the vehicle PlanningVehicle workingVehicle = problemChangeDirector.lookUpWorkingObjectOrFail(removedVehicle); // Un-initialize all visits of this vehicle for (PlanningVisit visit : workingVehicle.getFutureVisits()) { problemChangeDirector.changeVariable(visit, "previousStandstill", planningVisit -> planningVisit.setPreviousStandstill(null)); } // No need to clone the vehicleList because it is a planning entity collection, so it is already // planning-cloned. // To learn more about problem fact changes, see: // https://www.optaplanner.org/docs/optaplanner/latest/repeated-planning/repeated-planning.html#problemChangeExample // Remove the vehicle problemChangeDirector.removeProblemFact(workingVehicle, planningVehicle -> { if (!workingSolution.getVehicleList().remove(planningVehicle)) { throw new IllegalStateException( "Working solution's vehicleList " + workingSolution.getVehicleList() + " doesn't contain the workingVehicle (" + planningVehicle + "). This is a bug!"); } }); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/RemoveVisit.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import java.util.Objects; import org.optaplanner.core.api.solver.change.ProblemChange; import org.optaplanner.core.api.solver.change.ProblemChangeDirector; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; public class RemoveVisit implements ProblemChange { private final PlanningVisit planningVisit; public RemoveVisit(PlanningVisit planningVisit) { this.planningVisit = Objects.requireNonNull(planningVisit); } @Override public void doChange(VehicleRoutingSolution workingSolution, ProblemChangeDirector problemChangeDirector) { // Look up a working copy of the visit PlanningVisit workingVisit = problemChangeDirector.lookUpWorkingObjectOrFail(planningVisit); // Fix the next visit and set its previousStandstill to the removed visit's previousStandstill PlanningVisit nextVisit = workingVisit.getNextVisit(); if (nextVisit != null) { // otherwise it's the last visit problemChangeDirector.changeVariable(nextVisit, "previousStandstill", workingNextVisit -> workingNextVisit.setPreviousStandstill(workingVisit.getPreviousStandstill())); } // No need to clone the visitList because it is a planning entity collection, so it is already planning-cloned. // To learn more about problem fact changes, see: // https://www.optaplanner.org/docs/optaplanner/latest/repeated-planning/repeated-planning.html#problemChangeExample // Remove the visit problemChangeDirector.removeEntity(planningVisit, visit -> { if (!workingSolution.getVisitList().remove(visit)) { throw new IllegalStateException( "Working solution's visitList " + workingSolution.getVisitList() + " doesn't contain the workingVisit (" + visit + "). This is a bug!"); } }); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/change/package-info.java ================================================ /** * {@link org.optaplanner.core.api.solver.change.ProblemChange} implementations. *

* Problem fact changes are difficult to write correctly. To understand the code and when implementing new fact changes, * read * ProblemChange documentation. */ package org.optaweb.vehiclerouting.plugin.planner.change; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/DistanceMap.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; /** * Contains travel distances from a reference location to other locations. */ @FunctionalInterface public interface DistanceMap { /** * Get distance from a reference location to the given location. The actual physical quantity (distance or time) * and its units depend on the configuration of the routing engine and is not important for optimization. * * @param location location the distance of which will be returned * @return location's distance */ long distanceTo(PlanningLocation location); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningDepot.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import java.util.Objects; public class PlanningDepot { private final PlanningLocation location; public PlanningDepot(PlanningLocation location) { this.location = Objects.requireNonNull(location); } public long getId() { return location.getId(); } public PlanningLocation getLocation() { return location; } @Override public String toString() { return "PlanningDepot{" + "location=" + location.getId() + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningLocation.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import java.util.Objects; public class PlanningLocation { private final long id; // Only used to calculate angle. private final double latitude; private final double longitude; private final DistanceMap travelDistanceMap; PlanningLocation(long id, double latitude, double longitude, DistanceMap travelDistanceMap) { this.id = id; this.latitude = latitude; this.longitude = longitude; this.travelDistanceMap = Objects.requireNonNull(travelDistanceMap); } /** * ID of the corresponding domain location. * * @return domain location ID */ public long getId() { return id; } /** * Distance to the given location. * * @param location other location * @return distance to the other location */ public long distanceTo(PlanningLocation location) { if (this == location) { return 0L; } return travelDistanceMap.distanceTo(location); } /** * Angle between the given location and the direction EAST with {@code this} location being the vertex. * * @param location location that forms one side of the angle (not {@code null}) * @return angle in radians in the range of -π to π */ public double angleTo(PlanningLocation location) { // Euclidean distance (Pythagorean theorem) - not correct when the surface is a sphere double latitudeDifference = location.latitude - latitude; double longitudeDifference = location.longitude - longitude; return Math.atan2(latitudeDifference, longitudeDifference); } @Override public String toString() { return "PlanningLocation{" + "latitude=" + latitude + ",longitude=" + longitude + ",travelDistanceMap=" + travelDistanceMap + ",id=" + id + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningLocationFactory.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import org.optaweb.vehiclerouting.domain.Location; /** * Creates {@link PlanningLocation}s. */ public class PlanningLocationFactory { private PlanningLocationFactory() { throw new AssertionError("Utility class"); } /** * Create planning location without a distance map. This location cannot be used for planning but can be used for * a problem fact change to remove a visit. * * @param location domain location * @return planning location without distance map */ public static PlanningLocation fromDomain(Location location) { return fromDomain(location, PlanningLocationFactory::failFast); } /** * Create planning location from a domain location and a distance map. * * @param location domain location * @param distanceMap distance map of this planning location * @return planning location */ public static PlanningLocation fromDomain(Location location, DistanceMap distanceMap) { return new PlanningLocation( location.id(), location.coordinates().latitude().doubleValue(), location.coordinates().longitude().doubleValue(), distanceMap); } /** * Create test location without distance map and coordinates. Coordinates will be initialized to zero. * * @param id location ID * @return planning location without distance map and coordinates */ public static PlanningLocation testLocation(long id) { return testLocation(id, PlanningLocationFactory::failFast); } /** * Create test location with distance map and without coordinates. Coordinates will be initialized to zero. * * @param id location ID * @param distanceMap distance map * @return planning location with distance map and without coordinates */ public static PlanningLocation testLocation(long id, DistanceMap distanceMap) { return new PlanningLocation(id, 0, 0, distanceMap); } private static long failFast(PlanningLocation location) { throw new IllegalStateException(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVehicle.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import java.util.Iterator; import java.util.NoSuchElementException; import org.optaplanner.core.api.domain.lookup.PlanningId; public class PlanningVehicle implements Standstill { @PlanningId private long id; private int capacity; private PlanningDepot depot; // Shadow variables private PlanningVisit nextVisit; PlanningVehicle() { // Hide public constructor in favor of the factory. } public long getId() { return id; } public void setId(long id) { this.id = id; } public int getCapacity() { return capacity; } public void setCapacity(int capacity) { this.capacity = capacity; } public PlanningDepot getDepot() { return depot; } public void setDepot(PlanningDepot depot) { this.depot = depot; } @Override public PlanningVisit getNextVisit() { return nextVisit; } @Override public void setNextVisit(PlanningVisit nextVisit) { this.nextVisit = nextVisit; } public Iterable getFutureVisits() { return () -> new Iterator() { PlanningVisit nextVisit = getNextVisit(); @Override public boolean hasNext() { return nextVisit != null; } @Override public PlanningVisit next() { if (nextVisit == null) { throw new NoSuchElementException(); } PlanningVisit out = nextVisit; nextVisit = nextVisit.getNextVisit(); return out; } }; } @Override public PlanningLocation getLocation() { return depot.getLocation(); } @Override public String toString() { return "PlanningVehicle{" + "capacity=" + capacity + (depot == null ? "" : ",depot=" + depot.getId()) + (nextVisit == null ? "" : ",nextVisit=" + nextVisit.getId()) + ",id=" + id + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVehicleFactory.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import org.optaweb.vehiclerouting.domain.Vehicle; /** * Creates {@link PlanningVehicle} instances. */ public class PlanningVehicleFactory { private PlanningVehicleFactory() { throw new AssertionError("Utility class"); } /** * Create planning vehicle from domain vehicle. * * @param domainVehicle domain vehicle * @return planning vehicle */ public static PlanningVehicle fromDomain(Vehicle domainVehicle) { return vehicle(domainVehicle.id(), domainVehicle.capacity()); } /** * Create a testing vehicle with zero capacity. * * @param id vehicle's ID * @return new vehicle with zero capacity */ public static PlanningVehicle testVehicle(long id) { return vehicle(id, 0); } /** * Create a testing vehicle with capacity. * * @param id vehicle's ID * @return new vehicle with the given capacity */ public static PlanningVehicle testVehicle(long id, int capacity) { return vehicle(id, capacity); } private static PlanningVehicle vehicle(long id, int capacity) { PlanningVehicle vehicle = new PlanningVehicle(); vehicle.setId(id); vehicle.setCapacity(capacity); return vehicle; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVisit.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import org.optaplanner.core.api.domain.entity.PlanningEntity; import org.optaplanner.core.api.domain.lookup.PlanningId; import org.optaplanner.core.api.domain.variable.AnchorShadowVariable; import org.optaplanner.core.api.domain.variable.PlanningVariable; import org.optaplanner.core.api.domain.variable.PlanningVariableGraphType; import org.optaweb.vehiclerouting.plugin.planner.weight.DepotAngleVisitDifficultyWeightFactory; @PlanningEntity(difficultyWeightFactoryClass = DepotAngleVisitDifficultyWeightFactory.class) public class PlanningVisit implements Standstill { @PlanningId private long id; private PlanningLocation location; private int demand; // Planning variable: changes during planning, between score calculations. @PlanningVariable(valueRangeProviderRefs = { "vehicleRange", "visitRange" }, graphType = PlanningVariableGraphType.CHAINED) private Standstill previousStandstill; // Shadow variables private PlanningVisit nextVisit; @AnchorShadowVariable(sourceVariableName = "previousStandstill") private PlanningVehicle vehicle; PlanningVisit() { // Hide public constructor in favor of the factory. } public long getId() { return id; } public void setId(long id) { this.id = id; } @Override public PlanningLocation getLocation() { return location; } public void setLocation(PlanningLocation location) { this.location = location; } public int getDemand() { return demand; } public void setDemand(int demand) { this.demand = demand; } public Standstill getPreviousStandstill() { return previousStandstill; } public void setPreviousStandstill(Standstill previousStandstill) { this.previousStandstill = previousStandstill; } @Override public PlanningVisit getNextVisit() { return nextVisit; } @Override public void setNextVisit(PlanningVisit nextVisit) { this.nextVisit = nextVisit; } public PlanningVehicle getVehicle() { return vehicle; } public void setVehicle(PlanningVehicle vehicle) { this.vehicle = vehicle; } // ************************************************************************ // Complex methods // ************************************************************************ /** * Distance from the previous standstill to this visit. This is used to calculate the travel cost of a chain * beginning with a vehicle (at a depot) and ending with the {@link #isLast() last} visit. * The chain ends with a visit, not a depot so the cost of returning from the last visit back to the depot * has to be added in a separate step using {@link #distanceToDepot()}. * * @return distance from previous standstill to this visit */ public long distanceFromPreviousStandstill() { if (previousStandstill == null) { throw new IllegalStateException( "This method must not be called when the previousStandstill (null) is not initialized yet."); } return previousStandstill.getLocation().distanceTo(location); } /** * Distance from this visit back to the depot. * * @return distance from this visit back its vehicle's depot */ public long distanceToDepot() { return location.distanceTo(vehicle.getLocation()); } /** * Whether this visit is the last in a chain. * * @return true, if this visit has no {@link #getNextVisit() next} visit */ public boolean isLast() { return nextVisit == null; } @Override public String toString() { return "PlanningVisit{" + (location == null ? "" : "location=" + location.getId()) + ",demand=" + demand + (previousStandstill == null ? "" : ",previousStandstill='" + previousStandstill.getLocation().getId()) + (nextVisit == null ? "" : ",nextVisit=" + nextVisit.getId()) + (vehicle == null ? "" : ",vehicle=" + vehicle.getId()) + ",id=" + id + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVisitFactory.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; /** * Creates {@link PlanningVisit} instances. */ public class PlanningVisitFactory { static final int DEFAULT_VISIT_DEMAND = 1; private PlanningVisitFactory() { throw new AssertionError("Utility class"); } /** * Create visit with {@link #DEFAULT_VISIT_DEMAND}. * * @param location visit's location * @return new visit with the default demand */ public static PlanningVisit fromLocation(PlanningLocation location) { return fromLocation(location, DEFAULT_VISIT_DEMAND); } /** * Create visit of a location with the given demand. * * @param location visit's location * @param demand visit's demand * @return visit with demand at the given location */ public static PlanningVisit fromLocation(PlanningLocation location, int demand) { PlanningVisit visit = new PlanningVisit(); visit.setId(location.getId()); visit.setLocation(location); visit.setDemand(demand); return visit; } /** * Create a test visit with the given ID. * * @param id ID of the visit and its location * @return visit with an ID only */ public static PlanningVisit testVisit(long id) { return fromLocation(PlanningLocationFactory.testLocation(id)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/SolutionFactory.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import java.util.ArrayList; import java.util.List; import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; /** * Creates {@link VehicleRoutingSolution} instances. */ public class SolutionFactory { private SolutionFactory() { throw new AssertionError("Utility class"); } /** * Create an empty solution. Empty solution has zero locations, depots, visits and vehicles and a zero score. * * @return empty solution */ public static VehicleRoutingSolution emptySolution() { VehicleRoutingSolution solution = new VehicleRoutingSolution(); solution.setVisitList(new ArrayList<>()); solution.setDepotList(new ArrayList<>()); solution.setVehicleList(new ArrayList<>()); solution.setScore(HardSoftLongScore.ZERO); return solution; } /** * Create a new solution from given vehicles, depot and visits. * All vehicles will be placed in the depot. *

* The returned solution's vehicles and locations are new collections so modifying the solution * won't affect the collections given as arguments. *

* Elements of the argument collections are NOT cloned. * * @param vehicles vehicles * @param depot depot * @param visits visits * @return solution containing the given vehicles, depot, visits and their locations */ public static VehicleRoutingSolution solutionFromVisits( List vehicles, PlanningDepot depot, List visits) { VehicleRoutingSolution solution = new VehicleRoutingSolution(); solution.setVehicleList(new ArrayList<>(vehicles)); solution.setDepotList(new ArrayList<>(1)); if (depot != null) { solution.getDepotList().add(depot); moveAllVehiclesToDepot(vehicles, depot); } solution.setVisitList(new ArrayList<>(visits)); solution.setScore(HardSoftLongScore.ZERO); return solution; } private static void moveAllVehiclesToDepot(List vehicles, PlanningDepot depot) { vehicles.forEach(vehicle -> vehicle.setDepot(depot)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/Standstill.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import org.optaplanner.core.api.domain.entity.PlanningEntity; import org.optaplanner.core.api.domain.variable.InverseRelationShadowVariable; @PlanningEntity public interface Standstill { /** * The standstill's location. * * @return never {@code null} */ PlanningLocation getLocation(); /** * The next visit after this standstill. * * @return sometimes {@code null} */ @InverseRelationShadowVariable(sourceVariableName = "previousStandstill") PlanningVisit getNextVisit(); void setNextVisit(PlanningVisit nextVisit); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/VehicleRoutingSolution.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import java.util.List; import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty; import org.optaplanner.core.api.domain.solution.PlanningScore; import org.optaplanner.core.api.domain.solution.PlanningSolution; import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty; import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider; import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; @PlanningSolution public class VehicleRoutingSolution { @ProblemFactCollectionProperty private List depotList; @PlanningEntityCollectionProperty @ValueRangeProvider(id = "vehicleRange") private List vehicleList; @PlanningEntityCollectionProperty @ValueRangeProvider(id = "visitRange") private List visitList; @PlanningScore private HardSoftLongScore score; VehicleRoutingSolution() { // Hide public constructor in favor of the factory. } public List getDepotList() { return this.depotList; } public void setDepotList(List depotList) { this.depotList = depotList; } public List getVehicleList() { return this.vehicleList; } public void setVehicleList(List vehicleList) { this.vehicleList = vehicleList; } public List getVisitList() { return this.visitList; } public void setVisitList(List visitList) { this.visitList = visitList; } public HardSoftLongScore getScore() { return this.score; } public void setScore(HardSoftLongScore score) { this.score = score; } @Override public String toString() { return "VehicleRoutingSolution{" + "depotList=" + depotList + ", vehicleList=" + vehicleList + ", visitList=" + visitList + ", score=" + score + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/domain/package-info.java ================================================ /** * Domain model adjusted for OptaPlanner. */ package org.optaweb.vehiclerouting.plugin.planner.domain; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/package-info.java ================================================ /** * Route optimization. */ package org.optaweb.vehiclerouting.plugin.planner; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/weight/DepotAngleVisitDifficultyWeightFactory.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.weight; import static java.util.Comparator.comparingDouble; import static java.util.Comparator.comparingLong; import java.util.Comparator; import java.util.Objects; import org.optaplanner.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; /** * On large data sets, the constructed solution looks like pizza slices. * The order of the slices depends on the {@link PlanningLocation#angleTo} implementation. */ public class DepotAngleVisitDifficultyWeightFactory implements SelectionSorterWeightFactory { @Override public DepotAngleVisitDifficultyWeight createSorterWeight(VehicleRoutingSolution solution, PlanningVisit visit) { PlanningDepot depot = solution.getDepotList().get(0); return new DepotAngleVisitDifficultyWeight( visit, // angle of the line from visit to depot relative to visit→east visit.getLocation().angleTo(depot.getLocation()), visit.getLocation().distanceTo(depot.getLocation()) + depot.getLocation().distanceTo(visit.getLocation())); } static class DepotAngleVisitDifficultyWeight implements Comparable { private static final Comparator COMPARATOR = comparingDouble((DepotAngleVisitDifficultyWeight weight) -> weight.depotAngle) // Ascending (further from the depot are more difficult) .thenComparingLong(weight -> weight.depotRoundTripDistance) .thenComparing(weight -> weight.visit, comparingLong(PlanningVisit::getId)); private final PlanningVisit visit; private final double depotAngle; private final long depotRoundTripDistance; DepotAngleVisitDifficultyWeight(PlanningVisit visit, double depotAngle, long depotRoundTripDistance) { this.visit = visit; this.depotAngle = depotAngle; this.depotRoundTripDistance = depotRoundTripDistance; } @Override public int compareTo(DepotAngleVisitDifficultyWeight other) { return COMPARATOR.compare(this, other); } @Override public boolean equals(Object o) { if (!(o instanceof DepotAngleVisitDifficultyWeight)) { return false; } return compareTo((DepotAngleVisitDifficultyWeight) o) == 0; } @Override public int hashCode() { return Objects.hash(visit, depotAngle, depotRoundTripDistance); } @Override public String toString() { return "DepotAngleVisitDifficultyWeight{" + "visit=" + visit + ", depotAngle=" + depotAngle + ", depotRoundTripDistance=" + depotRoundTripDistance + '}'; } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/planner/weight/package-info.java ================================================ /** * Implements * * planning entity difficulty comparison * * or * * planning variable strength comparison * * to enable advanced * * Construction Heuristic * * algorithms or * * sorted selection * . */ package org.optaweb.vehiclerouting.plugin.planner.weight; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/ClearResource.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import javax.inject.Inject; import javax.ws.rs.POST; import javax.ws.rs.Path; import org.optaweb.vehiclerouting.service.location.LocationService; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; @Path("api/clear") public class ClearResource { private final LocationService locationService; private final VehicleService vehicleService; @Inject public ClearResource(LocationService locationService, VehicleService vehicleService) { this.locationService = locationService; this.vehicleService = vehicleService; } @POST public void clear() { // TODO do this in one step (=> new RoutingPlanService) vehicleService.removeAll(); locationService.removeAll(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/DataSetDownloadResource.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.optaweb.vehiclerouting.service.demo.DemoService; /** * Serves the current data set as a downloadable YAML file. */ @Path("api/dataset/export") @Produces("text/x-yaml") public class DataSetDownloadResource { private final DemoService demoService; DataSetDownloadResource(DemoService demoService) { this.demoService = demoService; } @GET public Response exportDataSet() throws IOException { String dataSet = demoService.exportDataSet(); byte[] dataSetBytes = dataSet.getBytes(StandardCharsets.UTF_8); try (InputStream is = new ByteArrayInputStream(dataSetBytes)) { return Response.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"vrp_data_set.yaml\"") .header(HttpHeaders.CONTENT_LENGTH, dataSetBytes.length) .type(new MediaType("text", "x-yaml", StandardCharsets.UTF_8.name())) .entity(is) .build(); } } // TODO exception handler } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/DemoResource.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import javax.inject.Inject; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import org.optaweb.vehiclerouting.service.demo.DemoService; @Path("api/demo/{name}") public class DemoResource { private final DemoService demoService; @Inject public DemoResource(DemoService demoService) { this.demoService = demoService; } /** * Load a demo data set. * * @param name data set name */ @POST public void loadDemo(@PathParam("name") String name) { demoService.loadDemo(name); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/LocationResource.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import javax.inject.Inject; import javax.transaction.Transactional; import javax.ws.rs.DELETE; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.plugin.rest.model.PortableLocation; import org.optaweb.vehiclerouting.service.location.LocationService; @Path("api/location") public class LocationResource { private final LocationService locationService; @Inject public LocationResource(LocationService locationService) { this.locationService = locationService; } /** * Create new location. * * @param request new location description */ @Transactional @POST public void addLocation(PortableLocation request) { locationService.createLocation( new Coordinates(request.getLatitude(), request.getLongitude()), request.getDescription()); } /** * Delete location. * * @param id ID of the location to be deleted */ @Transactional @DELETE @Path("{id}") public void deleteLocation(@PathParam("id") long id) { locationService.removeLocation(id); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/RouteEventResource.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import javax.annotation.PreDestroy; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.sse.OutboundSseEvent; import javax.ws.rs.sse.Sse; import javax.ws.rs.sse.SseBroadcaster; import javax.ws.rs.sse.SseEventSink; import org.optaweb.vehiclerouting.domain.RoutingPlan; import org.optaweb.vehiclerouting.plugin.rest.model.PortableErrorMessage; import org.optaweb.vehiclerouting.plugin.rest.model.PortableRoutingPlanFactory; import org.optaweb.vehiclerouting.service.error.ErrorMessage; import org.optaweb.vehiclerouting.service.route.RouteListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ApplicationScoped @Path("api/events") public class RouteEventResource { private static final Logger logger = LoggerFactory.getLogger(RouteEventResource.class); // TODO repository, not listener (service) private final RouteListener routeListener; private SseBroadcaster sseBroadcaster; private OutboundSseEvent.Builder eventBuilder; @Inject public RouteEventResource(RouteListener routeListener) { this.routeListener = routeListener; } // Handy during development. @PreDestroy public void closeBroadcaster() { if (sseBroadcaster != null) { logger.debug("Closing Server-Sent Events broadcaster."); sseBroadcaster.close(); } } public void observeRoute(@Observes RoutingPlan event) { if (sseBroadcaster != null) { sseBroadcaster.broadcast(eventBuilder .data(PortableRoutingPlanFactory.fromRoutingPlan(event)) .name("route") .comment("route update") .build()); } } public void observeError(@Observes ErrorMessage event) { if (sseBroadcaster != null) { sseBroadcaster.broadcast(eventBuilder .data(PortableErrorMessage.fromMessage(event)) .name("errorMessage") .comment("error message") .build()); } } @GET @Produces(MediaType.SERVER_SENT_EVENTS) public void sse(@Context Sse sse, @Context SseEventSink eventSink) { if (sseBroadcaster == null) { sseBroadcaster = sse.newBroadcaster(); eventBuilder = sse.newEventBuilder() .mediaType(MediaType.APPLICATION_JSON_TYPE) .reconnectDelay(3000); } OutboundSseEvent sseEvent = eventBuilder .data(PortableRoutingPlanFactory.fromRoutingPlan(routeListener.getBestRoutingPlan())) .comment("best route") .build(); eventSink.send(sseEvent); sseBroadcaster.register(eventSink); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/ServerInfoResource.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import static java.util.stream.Collectors.toList; import java.util.Arrays; import java.util.List; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.optaweb.vehiclerouting.plugin.rest.model.PortableCoordinates; import org.optaweb.vehiclerouting.plugin.rest.model.RoutingProblemInfo; import org.optaweb.vehiclerouting.plugin.rest.model.ServerInfo; import org.optaweb.vehiclerouting.service.demo.DemoService; import org.optaweb.vehiclerouting.service.region.BoundingBox; import org.optaweb.vehiclerouting.service.region.RegionService; @Path("api/serverInfo") public class ServerInfoResource { private final DemoService demoService; private final RegionService regionService; @Inject public ServerInfoResource(DemoService demoService, RegionService regionService) { this.demoService = demoService; this.regionService = regionService; } @GET @Produces(MediaType.APPLICATION_JSON) public ServerInfo serverInfo() { BoundingBox boundingBox = regionService.boundingBox(); List portableBoundingBox = Arrays.asList( PortableCoordinates.fromCoordinates(boundingBox.getSouthWest()), PortableCoordinates.fromCoordinates(boundingBox.getNorthEast())); List demos = demoService.demos().stream() .map(routingProblem -> new RoutingProblemInfo( routingProblem.name(), routingProblem.visits().size())) .collect(toList()); return new ServerInfo(portableBoundingBox, regionService.countryCodes(), demos); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/VehicleResource.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import javax.inject.Inject; import javax.ws.rs.DELETE; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; @Path("api/vehicle") public class VehicleResource { private final VehicleService vehicleService; @Inject public VehicleResource(VehicleService vehicleService) { this.vehicleService = vehicleService; } @POST public void addVehicle() { vehicleService.createVehicle(); } /** * Delete vehicle. * * @param id ID of the vehicle to be deleted */ @DELETE @Path("{id}") public void removeVehicle(@PathParam("id") long id) { vehicleService.removeVehicle(id); } @POST @Path("deleteAny") public void removeAnyVehicle() { vehicleService.removeAnyVehicle(); } @POST @Path("{id}/capacity") public void changeCapacity(@PathParam("id") long id, int capacity) { vehicleService.changeCapacity(id, capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableCoordinates.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Objects; import org.optaweb.vehiclerouting.domain.Coordinates; import com.fasterxml.jackson.annotation.JsonProperty; /** * {@link Coordinates} representation optimized for network transport. */ public class PortableCoordinates { /* * Five decimal places gives "metric" precision (±55 cm on equator). That's enough for visualising the track. * https://wiki.openstreetmap.org/wiki/Node#Structure */ private static final int LATLNG_SCALE = 5; @JsonProperty(value = "lat") private final BigDecimal latitude; @JsonProperty(value = "lng") private final BigDecimal longitude; public static PortableCoordinates fromCoordinates(Coordinates coordinates) { Objects.requireNonNull(coordinates, "coordinates must not be null"); return new PortableCoordinates( coordinates.latitude(), coordinates.longitude()); } private static BigDecimal scale(BigDecimal number) { return number.setScale(Math.min(number.scale(), LATLNG_SCALE), RoundingMode.HALF_EVEN).stripTrailingZeros(); } PortableCoordinates(BigDecimal latitude, BigDecimal longitude) { this.latitude = scale(latitude); this.longitude = scale(longitude); } public BigDecimal getLatitude() { return latitude; } public BigDecimal getLongitude() { return longitude; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PortableCoordinates that = (PortableCoordinates) o; return Objects.equals(latitude, that.latitude) && Objects.equals(longitude, that.longitude); } @Override public int hashCode() { return Objects.hash(latitude, longitude); } @Override public String toString() { return "PortableCoordinates{" + "latitude=" + latitude + ", longitude=" + longitude + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableDistance.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.util.Objects; import org.optaweb.vehiclerouting.domain.Distance; import com.fasterxml.jackson.annotation.JsonValue; /** * Portable representation of a {@link Distance distance}. */ public class PortableDistance { @JsonValue private final String distance; static PortableDistance fromDistance(Distance distance) { long seconds = (Objects.requireNonNull(distance).millis() + 500) / 1000; return new PortableDistance(String.format("%dh %dm %ds", seconds / 3600, seconds / 60 % 60, seconds % 60)); } private PortableDistance(String distance) { this.distance = distance; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PortableDistance that = (PortableDistance) o; return distance.equals(that.distance); } @Override public int hashCode() { return Objects.hash(distance); } @Override public String toString() { return "PortableDistance{" + "distance='" + distance + '\'' + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableErrorMessage.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.util.Objects; import org.optaweb.vehiclerouting.service.error.ErrorMessage; /** * Portable error message. */ public class PortableErrorMessage { private final String id; private final String text; public static PortableErrorMessage fromMessage(ErrorMessage message) { return new PortableErrorMessage(message.id, message.text); } PortableErrorMessage(String id, String text) { this.id = id; this.text = text; } public String getId() { return id; } public String getText() { return text; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PortableErrorMessage that = (PortableErrorMessage) o; return id.equals(that.id) && text.equals(that.text); } @Override public int hashCode() { return Objects.hash(id, text); } @Override public String toString() { return "PortableErrorMessage{" + "id='" + id + '\'' + ", text='" + text + '\'' + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableLocation.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.math.BigDecimal; import java.util.Objects; import org.optaweb.vehiclerouting.domain.Location; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; /** * {@link Location} representation convenient for marshalling. */ public class PortableLocation { private final long id; @JsonProperty(value = "lat", required = true) private final BigDecimal latitude; @JsonProperty(value = "lng", required = true) private final BigDecimal longitude; private final String description; static PortableLocation fromLocation(Location location) { Objects.requireNonNull(location, "location must not be null"); return new PortableLocation( location.id(), location.coordinates().latitude(), location.coordinates().longitude(), location.description()); } @JsonCreator public PortableLocation( @JsonProperty(value = "id") long id, @JsonProperty(value = "lat") BigDecimal latitude, @JsonProperty(value = "lng") BigDecimal longitude, @JsonProperty(value = "description") String description) { this.id = id; this.latitude = Objects.requireNonNull(latitude); this.longitude = Objects.requireNonNull(longitude); this.description = Objects.requireNonNull(description); } public long getId() { return id; } public BigDecimal getLatitude() { return latitude; } public BigDecimal getLongitude() { return longitude; } public String getDescription() { return description; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PortableLocation that = (PortableLocation) o; return id == that.id && description.equals(that.description) && latitude.compareTo(that.latitude) == 0 && longitude.compareTo(that.longitude) == 0; } @Override public int hashCode() { return Objects.hash(id, description, latitude, longitude); } @Override public String toString() { return "PortableLocation{" + "id=" + id + ", description='" + description + '\'' + ", latitude=" + latitude + ", longitude=" + longitude + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRoute.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.util.List; import java.util.Objects; import org.optaweb.vehiclerouting.domain.Route; import com.fasterxml.jackson.annotation.JsonFormat; /** * Vehicle {@link Route route} representation convenient for marshalling. */ class PortableRoute { private final PortableVehicle vehicle; private final PortableLocation depot; private final List visits; @JsonFormat(shape = JsonFormat.Shape.ARRAY) private final List> track; PortableRoute( PortableVehicle vehicle, PortableLocation depot, List visits, List> track) { this.vehicle = Objects.requireNonNull(vehicle); this.depot = Objects.requireNonNull(depot); this.visits = Objects.requireNonNull(visits); this.track = Objects.requireNonNull(track); } public PortableVehicle getVehicle() { return vehicle; } public PortableLocation getDepot() { return depot; } public List getVisits() { return visits; } public List> getTrack() { return track; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRoutingPlan.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.util.List; import org.optaweb.vehiclerouting.domain.RoutingPlan; /** * {@link RoutingPlan} representation convenient for marshalling. */ public class PortableRoutingPlan { private final PortableDistance distance; private final List vehicles; private final PortableLocation depot; private final List visits; private final List routes; PortableRoutingPlan( PortableDistance distance, List vehicles, PortableLocation depot, List visits, List routes) { // TODO require non-null this.distance = distance; this.vehicles = vehicles; this.depot = depot; this.visits = visits; this.routes = routes; } public PortableDistance getDistance() { return distance; } public List getVehicles() { return vehicles; } public PortableLocation getDepot() { return depot; } public List getVisits() { return visits; } public List getRoutes() { return routes; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRoutingPlanFactory.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static java.util.stream.Collectors.toList; import java.util.ArrayList; import java.util.List; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.RoutingPlan; import org.optaweb.vehiclerouting.domain.Vehicle; /** * Creates instances of {@link PortableRoutingPlan}. */ public class PortableRoutingPlanFactory { private PortableRoutingPlanFactory() { throw new AssertionError("Utility class"); } public static PortableRoutingPlan fromRoutingPlan(RoutingPlan routingPlan) { PortableDistance distance = PortableDistance.fromDistance(routingPlan.distance()); List vehicles = portableVehicles(routingPlan.vehicles()); PortableLocation depot = routingPlan.depot().map(PortableLocation::fromLocation).orElse(null); List visits = portableVisits(routingPlan.visits()); List routes = routingPlan.routes().stream() .map(routeWithTrack -> new PortableRoute( PortableVehicle.fromVehicle(routeWithTrack.vehicle()), depot, portableVisits(routeWithTrack.visits()), portableTrack(routeWithTrack.track()))) .collect(toList()); return new PortableRoutingPlan(distance, vehicles, depot, visits, routes); } private static List> portableTrack(List> track) { ArrayList> portableTrack = new ArrayList<>(); for (List segment : track) { List portableSegment = segment.stream() .map(PortableCoordinates::fromCoordinates) .collect(toList()); portableTrack.add(portableSegment); } return portableTrack; } private static List portableVisits(List visits) { return visits.stream() .map(PortableLocation::fromLocation) .collect(toList()); } private static List portableVehicles(List vehicles) { return vehicles.stream() .map(PortableVehicle::fromVehicle) .collect(toList()); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableVehicle.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.util.Objects; import org.optaweb.vehiclerouting.domain.Vehicle; /** * {@link Vehicle} representation suitable for network transport. */ public class PortableVehicle { private final long id; private final String name; private final int capacity; static PortableVehicle fromVehicle(Vehicle vehicle) { Objects.requireNonNull(vehicle, "vehicle must not be null"); return new PortableVehicle(vehicle.id(), vehicle.name(), vehicle.capacity()); } PortableVehicle(long id, String name, int capacity) { this.id = id; this.name = Objects.requireNonNull(name); this.capacity = capacity; } public long getId() { return id; } public String getName() { return name; } public int getCapacity() { return capacity; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PortableVehicle vehicle = (PortableVehicle) o; return id == vehicle.id && capacity == vehicle.capacity && name.equals(vehicle.name); } @Override public int hashCode() { return Objects.hash(id, name, capacity); } @Override public String toString() { return "PortableVehicle{" + "id=" + id + ", name='" + name + '\'' + ", capacity=" + capacity + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/RoutingProblemInfo.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.util.Objects; import org.optaweb.vehiclerouting.domain.RoutingProblem; /** * Information about a {@link RoutingProblem routing problem instance}. */ public class RoutingProblemInfo { private final String name; private final int visits; public RoutingProblemInfo(String name, int visits) { this.name = Objects.requireNonNull(name); this.visits = visits; } /** * Routing problem instance name. * * @return name */ public String getName() { return name; } /** * Number of visits in the routing problem instance. * * @return number of visits */ public int getVisits() { return visits; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/rest/model/ServerInfo.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import java.util.List; /** * Server info suitable for network transport. */ public class ServerInfo { private final List boundingBox; private final List countryCodes; private final List demos; public ServerInfo(List boundingBox, List countryCodes, List demos) { this.boundingBox = boundingBox; this.countryCodes = countryCodes; this.demos = demos; } public List getBoundingBox() { return boundingBox; } public List getCountryCodes() { return countryCodes; } public List getDemos() { return demos; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/AirDistanceRouter.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import javax.enterprise.context.ApplicationScoped; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.service.distance.DistanceCalculator; import org.optaweb.vehiclerouting.service.region.BoundingBox; import org.optaweb.vehiclerouting.service.region.Region; import org.optaweb.vehiclerouting.service.route.Router; import io.quarkus.arc.properties.IfBuildProperty; @ApplicationScoped @IfBuildProperty(name = "app.routing.engine", stringValue = "AIR") public class AirDistanceRouter implements Router, DistanceCalculator, Region { protected static final int TRAVEL_SPEED_KPH = 60; // Approximate Metric Equivalents for Degrees. At the equator for longitude and for latitude anywhere, // the following approximations are valid: 1° = 111 km (or 60 nautical miles) 0.1° = 11.1 km. protected static final double KILOMETERS_PER_DEGREE = 111; protected static final long MILLIS_IN_ONE_HOUR = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); @Override public long travelTimeMillis(Coordinates from, Coordinates to) { BigDecimal latDiff = to.latitude().subtract(from.latitude()); BigDecimal lngDiff = to.longitude().subtract(from.longitude()); double distanceKilometers = Math.sqrt(latDiff.pow(2).add(lngDiff.pow(2)).doubleValue()) * KILOMETERS_PER_DEGREE; return (long) Math.floor(distanceKilometers / TRAVEL_SPEED_KPH * MILLIS_IN_ONE_HOUR); } @Override public List getPath(Coordinates from, Coordinates to) { return Arrays.asList(from, to); } @Override public BoundingBox getBounds() { return new BoundingBox(Coordinates.of(-90, -180), Coordinates.of(90, 180)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/Constants.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; public class Constants { public static final String GRAPHHOPPER_PROFILE = "optaweb_car"; } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/GraphHopperRouter.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import static java.util.stream.Collectors.toList; import java.util.List; import java.util.stream.StreamSupport; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.service.distance.DistanceCalculator; import org.optaweb.vehiclerouting.service.distance.RoutingException; import org.optaweb.vehiclerouting.service.region.BoundingBox; import org.optaweb.vehiclerouting.service.region.Region; import org.optaweb.vehiclerouting.service.route.Router; import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopper; import com.graphhopper.ResponsePath; import com.graphhopper.util.PointList; import com.graphhopper.util.shapes.BBox; import io.quarkus.arc.properties.IfBuildProperty; /** * Provides geographical information needed for route optimization. */ @ApplicationScoped @IfBuildProperty(name = "app.routing.engine", stringValue = "GRAPHHOPPER", enableIfMissing = true) class GraphHopperRouter implements Router, DistanceCalculator, Region { private final GraphHopper graphHopper; @Inject GraphHopperRouter(GraphHopper graphHopper) { this.graphHopper = graphHopper; } @Override public List getPath(Coordinates from, Coordinates to) { PointList points = getBestRoute(from, to).getPoints(); return StreamSupport.stream(points.spliterator(), false) .map(ghPoint3D -> Coordinates.of(ghPoint3D.lat, ghPoint3D.lon)) .collect(toList()); } @Override public long travelTimeMillis(Coordinates from, Coordinates to) { return getBestRoute(from, to).getTime(); } private ResponsePath getBestRoute(Coordinates from, Coordinates to) { GHRequest request = new GHRequest( from.latitude().doubleValue(), from.longitude().doubleValue(), to.latitude().doubleValue(), to.longitude().doubleValue()).setProfile(Constants.GRAPHHOPPER_PROFILE); GHResponse response = graphHopper.route(request); // TODO return wrapper that can hold both the result and error explanation instead of throwing exception if (response.hasErrors()) { throw new RoutingException("No route from (" + from + ") to (" + to + ")", response.getErrors().get(0)); } return response.getBest(); } @Override public BoundingBox getBounds() { BBox bounds = graphHopper.getBaseGraph().getBounds(); return new BoundingBox( Coordinates.of(bounds.minLat, bounds.minLon), Coordinates.of(bounds.maxLat, bounds.maxLon)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/RoutingConfig.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.stream.Stream; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import javax.inject.Inject; import org.optaweb.vehiclerouting.Profiles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.graphhopper.GraphHopper; import com.graphhopper.config.CHProfile; import com.graphhopper.config.Profile; import io.quarkus.arc.DefaultBean; import io.quarkus.arc.profile.UnlessBuildProfile; import io.quarkus.arc.properties.IfBuildProperty; /** * Configuration bean that creates a GraphHopper instance and allows to configure the path to OSM file * through environment. */ @Dependent class RoutingConfig { private static final Logger logger = LoggerFactory.getLogger(RoutingConfig.class); private final Path osmDir; private final Path osmFile; private final Optional osmDownloadUrl; private final Path graphHopperDir; private final Path graphDir; @Inject RoutingConfig(RoutingProperties routingProperties) { osmDir = Paths.get(routingProperties.osmDir()).toAbsolutePath(); osmFile = osmDir.resolve(routingProperties.osmFile()).toAbsolutePath(); osmDownloadUrl = routingProperties.osmDownloadUrl(); graphHopperDir = Paths.get(routingProperties.ghDir()); String regionName = routingProperties.osmFile().replaceFirst("\\.osm\\.pbf$", ""); graphDir = graphHopperDir.resolve(regionName).toAbsolutePath(); } /** * Avoids creating a real GraphHopper instance when running a @QuarkusTest. * * @return real GraphHopper */ @UnlessBuildProfile(Profiles.TEST) @IfBuildProperty(name = "app.routing.engine", stringValue = "GRAPHHOPPER", enableIfMissing = true) @Produces @DefaultBean GraphHopper graphHopper() { GraphHopper graphHopper = new GraphHopper(); graphHopper.setGraphHopperLocation(graphDir.toString()); if (graphDirIsNotEmpty()) { logger.info("Loading existing GraphHopper graph from: {}", graphDir); } else { if (Files.notExists(osmFile)) { initDirs(); if (!osmDownloadUrl.isPresent() || osmDownloadUrl.get().trim().isEmpty()) { throw new IllegalStateException( "The osmFile (" + osmFile + ") does not exist" + " and no download URL was provided.\n" + "Download the OSM file from https://download.geofabrik.de/ first" + " or provide an OSM file URL" + " using the app.routing.osm-download-url property."); } downloadOsmFile(osmDownloadUrl.get(), osmFile); } logger.info("Importing OSM file: {}", osmFile); graphHopper.setOSMFile(osmFile.toString()); } /* * Define a profile for each type of request that's going to be made at runtime. We're only going to ask for the fastest * route for a car, so we only need one profile. * * Change the weighting to "shortest" (and delete the graph directory to re-import it) to optimize for shortest routes. * * Add a second profile with "shortest" weighting (and delete the graph directory) to be able to change travel cost * optimization goal at runtime. */ graphHopper.setProfiles(new Profile(Constants.GRAPHHOPPER_PROFILE).setVehicle("car").setWeighting("fastest")); /* * Quick overview of routing modes: * * Flexible mode: * - Dijkstra or A* * - able to change requirements per request * Speed mode: * - "Contraction Hierarchies" algorithm (CH) * - still Dijkstra but on a "shortcut graph" * Hybrid mode * - landmark algorithm * - flexible and fast * * See https://www.graphhopper.com/blog/2017/08/14/flexible-routing-15-times-faster/. */ // Use CH for the only profile we have. graphHopper.getCHPreparationHandler().setCHProfiles(new CHProfile(Constants.GRAPHHOPPER_PROFILE)); graphHopper.importOrLoad(); logger.info("GraphHopper graph loaded"); return graphHopper; } /** * Decide whether the graph can be loaded. * * @return true if the graph directory exists and is not empty */ private boolean graphDirIsNotEmpty() { if (Files.notExists(graphDir)) { return false; } try (Stream graphDirFiles = Files.list(graphDir)) { // Defensive programming. Check if the graph dir is empty. That happens if the import fails // for example due to OutOfMemoryError. return graphDirFiles.findAny().isPresent(); } catch (IOException e) { throw new RoutingEngineException("Cannot read contents of the graph directory (" + graphDir + ")", e); } } private void initDirs() { try { Files.createDirectories(osmDir); Files.createDirectories(graphHopperDir); } catch (IOException e) { throw new RoutingEngineException("Can't create directory for storing OSM download", e); } } static void downloadOsmFile(String urlString, Path osmFile) { HttpURLConnection con; URL url; try { url = new URL(urlString); con = (HttpURLConnection) url.openConnection(); } catch (MalformedURLException e) { throw new RoutingEngineException("The OSM file URL is malformed", e); } catch (IOException e) { throw new RoutingEngineException("The OSM file cannot be downloaded", e); } try { con.setRequestMethod("GET"); } catch (ProtocolException e) { throw new IllegalStateException("Can't set request method", e); } con.setConnectTimeout(10000); con.setReadTimeout(10000); logger.info("Downloading OSM file from {}", urlString); try { Files.copy(con.getInputStream(), osmFile); } catch (IOException e) { throw new RoutingEngineException("OSM file download failed", e); } logger.info("File saved to {}", osmFile); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/RoutingEngineException.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; public class RoutingEngineException extends RuntimeException { RoutingEngineException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/RoutingProperties.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import java.util.Optional; import io.smallrye.config.ConfigMapping; @ConfigMapping(prefix = "app.routing") public interface RoutingProperties { /** * Directory to read OSM files from. */ String osmDir(); /** * Directory where GraphHopper graphs are stored. */ String ghDir(); /** * OpenStreetMap file name. */ String osmFile(); /** * URL of an .osm.pbf file that will be downloaded in case the file doesn't exist on the file system. */ Optional osmDownloadUrl(); /** * Routing engine providing distances and paths. */ RoutingEngine engine(); enum RoutingEngine { AIR, GRAPHHOPPER } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/plugin/routing/package-info.java ================================================ /** * Provides information based on geographical data, for example fastest and shortest distances between locations. */ package org.optaweb.vehiclerouting.plugin.routing; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/DemoProperties.java ================================================ package org.optaweb.vehiclerouting.service.demo; import java.util.Optional; import io.smallrye.config.ConfigMapping; @ConfigMapping(prefix = "app.demo") public interface DemoProperties { /** * Directory with demo data sets. */ Optional dataSetDir(); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/DemoService.java ================================================ package org.optaweb.vehiclerouting.service.demo; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.RoutingProblem; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.service.demo.dataset.DataSetMarshaller; import org.optaweb.vehiclerouting.service.location.LocationRepository; import org.optaweb.vehiclerouting.service.location.LocationService; import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; /** * Performs demo-related use cases. */ @ApplicationScoped public class DemoService { static final int MAX_TRIES = 10; private final RoutingProblemList routingProblems; private final LocationService locationService; private final LocationRepository locationRepository; private final VehicleService vehicleService; private final VehicleRepository vehicleRepository; private final DataSetMarshaller dataSetMarshaller; @Inject public DemoService( RoutingProblemList routingProblems, LocationService locationService, LocationRepository locationRepository, VehicleService vehicleService, VehicleRepository vehicleRepository, DataSetMarshaller dataSetMarshaller) { this.routingProblems = routingProblems; this.locationService = locationService; this.locationRepository = locationRepository; this.vehicleService = vehicleService; this.vehicleRepository = vehicleRepository; this.dataSetMarshaller = dataSetMarshaller; } public Collection demos() { return routingProblems.all(); } public void loadDemo(String name) { RoutingProblem routingProblem = routingProblems.byName(name); // Add depot routingProblem.depot().ifPresent(depot -> addWithRetry(depot.coordinates(), depot.description())); // TODO start randomizing only after using all available cities (=> reproducibility for small demos) routingProblem.visits().forEach(visit -> addWithRetry(visit.coordinates(), visit.description())); routingProblem.vehicles().forEach(vehicleService::createVehicle); } private void addWithRetry(Coordinates coordinates, String description) { int tries = 0; while (tries < MAX_TRIES && !locationService.createLocation(coordinates, description).isPresent()) { tries++; } if (tries == MAX_TRIES) { throw new RuntimeException( "Impossible to create a new location near " + coordinates + " after " + tries + " attempts"); } } public String exportDataSet() { // FIXME still relying on the fact that the first location in the repository is the depot List visits = new ArrayList<>(locationRepository.locations()); Location depot = visits.isEmpty() ? null : visits.remove(0); List vehicles = vehicleRepository.vehicles(); return dataSetMarshaller.marshal(new RoutingProblem( "Custom Vehicle Routing instance", vehicles, depot, visits)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/RoutingProblemConfig.java ================================================ package org.optaweb.vehiclerouting.service.demo; import static java.util.stream.Collectors.toList; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.RoutingProblem; import org.optaweb.vehiclerouting.service.demo.dataset.DataSetMarshaller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Configuration bean that produces the list of available routing problem data sets. */ @Dependent class RoutingProblemConfig { private static final Logger logger = LoggerFactory.getLogger(RoutingProblemConfig.class); private final DemoProperties demoProperties; private final DataSetMarshaller dataSetMarshaller; @Inject RoutingProblemConfig(DemoProperties demoProperties, DataSetMarshaller dataSetMarshaller) { this.demoProperties = demoProperties; this.dataSetMarshaller = dataSetMarshaller; } @Produces RoutingProblemList routingProblems() { Stream allProblems = Stream.concat(classPathProblems(), dataSetDirProblems()); return new RoutingProblemList(allProblems); } private Stream classPathProblems() { return Stream.of(belgiumReader()).map(dataSetMarshaller::unmarshal); } private Stream dataSetDirProblems() { return dataSetDir().map(dir -> collectProblems(dir).stream()).orElse(Stream.empty()); } private List collectProblems(Path dataSetDirPath) { try (Stream dataSetPaths = Files.list(dataSetDirPath)) { return dataSetPaths .map(Path::toFile) .filter(file -> file.getName().endsWith(".yaml") && file.exists() && file.canRead()) .map(file -> { try { return new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8); } catch (FileNotFoundException e) { logger.error("Problem with dataset file {}", file, e); return null; } }) .filter(Objects::nonNull) // TODO make unmarshalling exception checked, catch it and ignore broken files .map(dataSetMarshaller::unmarshal) // Returning the stream here has no point because the stream is always closed by the try-with-resources. .collect(toList()); } catch (IOException e) { throw new IllegalStateException("Cannot list directory " + dataSetDirPath, e); } } private Optional dataSetDir() { // TODO watch the dir (and make this a service that has local/data resource as a dependency -> is testable) Optional dataSetDirProperty = demoProperties.dataSetDir(); if (!dataSetDirProperty.isPresent()) { logger.info("Data set directory (app.demo.data-set-dir) is not set."); return Optional.empty(); } Path dataSetDirPath = Paths.get(dataSetDirProperty.get()); if (!isReadableDir(dataSetDirPath)) { logger.warn( "Data set directory '{}' doesn't exist or cannot be read. No external data sets will be loaded.", dataSetDirPath.toAbsolutePath()); return Optional.empty(); } return Optional.of(dataSetDirPath); } private static Reader belgiumReader() { return new InputStreamReader( DemoService.class.getResourceAsStream("belgium-cities.yaml"), StandardCharsets.UTF_8); } private static boolean isReadableDir(Path path) { File file = path.toFile(); return file.exists() && file.canRead() && file.isDirectory(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/RoutingProblemList.java ================================================ package org.optaweb.vehiclerouting.service.demo; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; import java.util.Collection; import java.util.Map; import java.util.Objects; import java.util.stream.Stream; import org.optaweb.vehiclerouting.domain.RoutingProblem; /** * Utility class that holds a map of routing problem instances and allows to look them up by name. */ class RoutingProblemList { private final Map routingProblems; RoutingProblemList(Stream routingProblems) { this.routingProblems = Objects.requireNonNull(routingProblems) // TODO use file name as the key (that's more likely to be unique than data set name) .collect(toMap(RoutingProblem::name, identity())); } Collection all() { return routingProblems.values(); } RoutingProblem byName(String name) { RoutingProblem routingProblem = routingProblems.get(name); if (routingProblem == null) { throw new IllegalArgumentException("Data set with name '" + name + "' doesn't exist"); } return routingProblem; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSet.java ================================================ package org.optaweb.vehiclerouting.service.demo.dataset; import java.util.List; /** * Data set representation used for marshalling and unmarshalling. */ class DataSet { private String name; private List vehicles; private DataSetLocation depot; private List visits; /** * Data set name (a short description). * * @return data set name (may be {@code null}) */ public String getName() { return name; } public void setName(String name) { this.name = name; } /** * Vehicles. * * @return vehicles (may be {@code null}) */ public List getVehicles() { return vehicles; } public void setVehicles(List vehicles) { this.vehicles = vehicles; } /** * The depot. * * @return the depot (may be {@code null}) */ public DataSetLocation getDepot() { return depot; } public void setDepot(DataSetLocation depot) { this.depot = depot; } /** * Visits. * * @return visits (may be {@code null}) */ public List getVisits() { return visits; } public void setVisits(List visits) { this.visits = visits; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSetLocation.java ================================================ package org.optaweb.vehiclerouting.service.demo.dataset; import com.fasterxml.jackson.annotation.JsonProperty; /** * Data set location. */ class DataSetLocation { private String label; @JsonProperty(value = "lat") private double latitude; @JsonProperty(value = "lng") private double longitude; private DataSetLocation() { // for unmarshalling } DataSetLocation(String label, double latitude, double longitude) { this.latitude = latitude; this.longitude = longitude; this.label = label; } /** * Location label. * * @return label */ public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } /** * Latitude. * * @return latitude */ public double getLatitude() { return latitude; } public void setLatitude(double latitude) { this.latitude = latitude; } /** * Longitude. * * @return longitude */ public double getLongitude() { return longitude; } public void setLongitude(double longitude) { this.longitude = longitude; } @Override public String toString() { return "DataSetLocation{" + "label='" + label + '\'' + ", latitude=" + latitude + ", longitude=" + longitude + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSetMarshaller.java ================================================ package org.optaweb.vehiclerouting.service.demo.dataset; import static java.util.stream.Collectors.toList; import java.io.IOException; import java.io.Reader; import java.util.Collections; import java.util.Optional; import javax.enterprise.context.ApplicationScoped; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.LocationData; import org.optaweb.vehiclerouting.domain.RoutingProblem; import org.optaweb.vehiclerouting.domain.VehicleData; import org.optaweb.vehiclerouting.domain.VehicleFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; /** * Data set marshaller using the YAML format. */ @ApplicationScoped public class DataSetMarshaller { private final ObjectMapper mapper; /** * Create marshaller using the default object mapper, which is set up to use YAML format. */ DataSetMarshaller() { mapper = new ObjectMapper(new YAMLFactory()); } /** * Constructor for testing purposes. * * @param mapper usually a mock object mapper */ DataSetMarshaller(ObjectMapper mapper) { this.mapper = mapper; } /** * Unmarshal routing problem from a reader. * * @param reader a reader * @return routing problem */ public RoutingProblem unmarshal(Reader reader) { // TODO throw a checked exception that will force the caller to handle the reading problem // (e.g. a bad format) and report it to the user or log an error return toDomain(unmarshalToDataSet(reader)); } /** * Marshal routing problem to string. * * @param routingProblem routing problem * @return string containing the marshaled routing problem */ public String marshal(RoutingProblem routingProblem) { return marshal(toDataSet(routingProblem)); } DataSet unmarshalToDataSet(Reader reader) { try { return mapper.readValue(reader, DataSet.class); } catch (IOException e) { throw new IllegalStateException("Can't read demo data set", e); } } String marshal(DataSet dataSet) { try { return mapper.writeValueAsString(dataSet); } catch (JsonProcessingException e) { throw new IllegalStateException("Failed to marshal data set (" + dataSet.getName() + ")", e); } } static DataSet toDataSet(RoutingProblem routingProblem) { DataSet dataSet = new DataSet(); dataSet.setName(routingProblem.name()); dataSet.setDepot(routingProblem.depot().map(DataSetMarshaller::toDataSet).orElse(null)); dataSet.setVehicles(routingProblem.vehicles().stream() .map(DataSetMarshaller::toDataSet) .collect(toList())); dataSet.setVisits(routingProblem.visits().stream() .map(DataSetMarshaller::toDataSet) .collect(toList())); return dataSet; } static DataSetLocation toDataSet(LocationData locationData) { return new DataSetLocation( locationData.description(), locationData.coordinates().latitude().doubleValue(), locationData.coordinates().longitude().doubleValue()); } static DataSetVehicle toDataSet(VehicleData vehicleData) { return new DataSetVehicle(vehicleData.name(), vehicleData.capacity()); } static RoutingProblem toDomain(DataSet dataSet) { return new RoutingProblem( Optional.ofNullable(dataSet.getName()).orElse(""), Optional.ofNullable(dataSet.getVehicles()).orElse(Collections.emptyList()) .stream() .map(DataSetMarshaller::toDomain) .collect(toList()), Optional.ofNullable(dataSet.getDepot()).map(DataSetMarshaller::toDomain).orElse(null), Optional.ofNullable(dataSet.getVisits()).orElse(Collections.emptyList()) .stream() .map(DataSetMarshaller::toDomain) .collect(toList())); } static LocationData toDomain(DataSetLocation dataSetLocation) { return new LocationData( Coordinates.of(dataSetLocation.getLatitude(), dataSetLocation.getLongitude()), dataSetLocation.getLabel()); } static VehicleData toDomain(DataSetVehicle dataSetVehicle) { return VehicleFactory.vehicleData(dataSetVehicle.name, dataSetVehicle.capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSetVehicle.java ================================================ package org.optaweb.vehiclerouting.service.demo.dataset; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; /** * Data set vehicle. */ public class DataSetVehicle { @JsonProperty final String name; @JsonProperty final int capacity; @JsonCreator public DataSetVehicle(@JsonProperty("name") String name, @JsonProperty("capacity") int capacity) { this.name = name; this.capacity = capacity; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/dataset/package-info.java ================================================ /** * Data set marshalling and unmarshalling. */ package org.optaweb.vehiclerouting.service.demo.dataset; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/demo/package-info.java ================================================ /** * Demo data set loading and exporting. */ package org.optaweb.vehiclerouting.service.demo; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/DistanceCalculator.java ================================================ package org.optaweb.vehiclerouting.service.distance; import org.optaweb.vehiclerouting.domain.Coordinates; /** * Calculates distances between coordinates. */ public interface DistanceCalculator { /** * Calculate travel time in milliseconds. * * @param from origin * @param to destination * @return travel time in milliseconds * @throws RoutingException when the distance between given coordinates cannot be calculated */ long travelTimeMillis(Coordinates from, Coordinates to); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/DistanceMatrixImpl.java ================================================ package org.optaweb.vehiclerouting.service.distance; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.service.location.DistanceMatrix; import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow; @ApplicationScoped class DistanceMatrixImpl implements DistanceMatrix { private final DistanceCalculator distanceCalculator; private final Map> matrix = new HashMap<>(); @Inject DistanceMatrixImpl(DistanceCalculator distanceCalculator) { this.distanceCalculator = distanceCalculator; } @Override public DistanceMatrixRow addLocation(Location newLocation) { Map distancesToOthers = updateMatrixLazily(newLocation); return locationId -> distancesToOthers.computeIfAbsent(locationId, wrongId -> { throw new IllegalArgumentException( "Distance from " + newLocation + " to " + wrongId + " hasn't been recorded.\n" + "We only know distances to " + distancesToOthers.keySet()); }); } private Map updateMatrixLazily(Location location) { // Matrix == distance rows. // We're adding a whole new row with distances from the new location to existing ones. // We're also creating a new column by "appending" a new cell to each existing row. // This new column contains distances from each existing location to the new one. return matrix.computeIfAbsent(location, newLocation -> { // The map must be thread-safe because: // - we're updating it from the parallel stream below // - it is accessed from solver thread! Map distancesToOthers = new ConcurrentHashMap<>(); // the new row // distance to self is 0 distancesToOthers.put(newLocation.id(), Distance.ZERO); // For all entries (rows) in the matrix: matrix.entrySet().stream().parallel().forEach(distanceRow -> { // Entry key is the existing (other) location. Location other = distanceRow.getKey(); // Entry value is the data (cells) in the row (distances from the entry key location to any other). Map distancesFromOther = distanceRow.getValue(); // Add a new cell to the row with the distance from the entry key location to the new location // (results in a new column at the end of the loop). distancesFromOther.put(newLocation.id(), calculateDistance(other, newLocation)); // Add a cell to the new distance's row. distancesToOthers.put(other.id(), calculateDistance(newLocation, other)); }); return distancesToOthers; }); } private Distance calculateDistance(Location from, Location to) { return Distance.ofMillis(distanceCalculator.travelTimeMillis(from.coordinates(), to.coordinates())); } @Override public Distance distance(Location from, Location to) { if (!matrix.containsKey(from)) { throw new IllegalArgumentException("Unknown 'from' location (" + from + ")"); } Map distanceRow = matrix.get(from); if (!distanceRow.containsKey(to.id())) { throw new IllegalArgumentException("Unknown 'to' location (" + to + ")"); } return distanceRow.get(to.id()); } @Override public void put(Location from, Location to, Distance distance) { matrix.computeIfAbsent(from, location -> new ConcurrentHashMap<>()).put(to.id(), distance); } @Override public void removeLocation(Location location) { // Remove the distance matrix row (distances from the removed location to others). matrix.remove(location); // TODO also remove the "column" of the matrix (distances from others to the removed location) to avoid memory // leak. // But this probably requires making DistanceMatrixRow immutable (otherwise there's a risk of NPEs in solver) // and update PlanningLocations' distance maps through problem fact changes. } @Override public void clear() { matrix.clear(); } /** * Number of rows in the matrix. * * @return number of rows */ public int dimension() { return matrix.size(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/DistanceRepository.java ================================================ package org.optaweb.vehiclerouting.service.distance; import java.util.Optional; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; /** * Stores distances between locations. */ public interface DistanceRepository { void saveDistance(Location from, Location to, Distance distance); Optional getDistance(Location from, Location to); void deleteDistances(Location location); void deleteAll(); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/RoutingException.java ================================================ package org.optaweb.vehiclerouting.service.distance; public class RoutingException extends RuntimeException { public RoutingException(String message, Throwable cause) { super(message, cause); } public RoutingException(String message) { super(message); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/distance/package-info.java ================================================ /** * Distance matrix calculation. */ package org.optaweb.vehiclerouting.service.distance; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorEvent.java ================================================ package org.optaweb.vehiclerouting.service.error; import java.util.Objects; public class ErrorEvent { public final String message; /** * Create a new {@code ApplicationEvent}. * * @param source the object on which the event initially occurred or with * which the event is associated (never {@code null}) * @param message error message (never {@code null}) */ public ErrorEvent(Object source, String message) { this.message = Objects.requireNonNull(message); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorListener.java ================================================ package org.optaweb.vehiclerouting.service.error; import java.util.UUID; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.inject.Inject; /** * Creates messages from error events and passes them to consumers. */ @ApplicationScoped public class ErrorListener { private final Event errorMessageEvent; @Inject public ErrorListener(Event errorMessageEvent) { this.errorMessageEvent = errorMessageEvent; } public void onErrorEvent(@Observes ErrorEvent event) { errorMessageEvent.fire(ErrorMessage.of(UUID.randomUUID().toString(), event.message)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorMessage.java ================================================ package org.optaweb.vehiclerouting.service.error; import java.util.Objects; public class ErrorMessage { /** * Message ID (never {@code null}). */ public final String id; /** * Message text (never {@code null}). */ public final String text; public static ErrorMessage of(String id, String text) { return new ErrorMessage(id, text); } private ErrorMessage(String id, String text) { this.id = Objects.requireNonNull(id); this.text = Objects.requireNonNull(text); } @Override public String toString() { return "ErrorMessage{" + "id='" + id + '\'' + ", text='" + text + '\'' + '}'; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/ErrorMessageConsumer.java ================================================ package org.optaweb.vehiclerouting.service.error; /** * Consumes error messages. */ public interface ErrorMessageConsumer { /** * Consume an error message. * * @param message error message */ void consumeMessage(ErrorMessage message); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/error/package-info.java ================================================ /** * Handles error events. For example an uncaught exception can be turned into an error event that is sent to the client. */ package org.optaweb.vehiclerouting.service.error; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/DistanceMatrix.java ================================================ package org.optaweb.vehiclerouting.service.location; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; /** * Holds distances between every pair of locations. */ public interface DistanceMatrix { DistanceMatrixRow addLocation(Location location); void removeLocation(Location location); void clear(); Distance distance(Location from, Location to); void put(Location from, Location to, Distance distance); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/DistanceMatrixRow.java ================================================ package org.optaweb.vehiclerouting.service.location; import org.optaweb.vehiclerouting.domain.Distance; /** * Contains {@link Distance distances} from the location associated with this row to other locations. */ public interface DistanceMatrixRow { /** * Distance from this row's location to the given location. * * @param locationId target location * @return time it takes to travel to the given location */ Distance distanceTo(long locationId); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/LocationPlanner.java ================================================ package org.optaweb.vehiclerouting.service.location; import org.optaweb.vehiclerouting.domain.Location; /** * Optimizes the routing plan in response to location-related changes in the routing problem. */ public interface LocationPlanner { void addLocation(Location location, DistanceMatrixRow distanceMatrixRow); void removeLocation(Location location); void removeAllLocations(); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/LocationRepository.java ================================================ package org.optaweb.vehiclerouting.service.location; import java.util.List; import java.util.Optional; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; /** * Defines repository operations on locations. */ public interface LocationRepository { /** * Create a location with a unique ID. * * @param coordinates location's coordinates * @param description description of the location * @return a new location */ Location createLocation(Coordinates coordinates, String description); /** * Get all locations. * * @return all locations */ List locations(); /** * Remove a location with the given ID. * * @param id location ID * @return the removed location */ Location removeLocation(long id); /** * Remove all locations from the repository. */ void removeAll(); /** * Find a location by its ID. * * @param locationId location's ID * @return an Optional containing location with the given ID or empty Optional if there is no location with such ID */ Optional find(long locationId); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/LocationService.java ================================================ package org.optaweb.vehiclerouting.service.location; import static java.util.Comparator.comparingLong; import java.util.List; import java.util.Objects; import java.util.Optional; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.inject.Inject; import javax.transaction.Transactional; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.service.distance.DistanceRepository; import org.optaweb.vehiclerouting.service.error.ErrorEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Performs location-related use cases. */ @ApplicationScoped public class LocationService { private static final Logger logger = LoggerFactory.getLogger(LocationService.class); private final LocationRepository repository; private final DistanceRepository distanceRepository; private final LocationPlanner planner; // TODO move to RoutingPlanService (SRP) private final DistanceMatrix distanceMatrix; private final Event errorEvent; @Inject LocationService( LocationRepository repository, DistanceRepository distanceRepository, LocationPlanner planner, DistanceMatrix distanceMatrix, Event errorEvent) { this.repository = repository; this.distanceRepository = distanceRepository; this.planner = planner; this.distanceMatrix = distanceMatrix; this.errorEvent = errorEvent; } public synchronized void addLocation(Location location) { Objects.requireNonNull(location); DistanceMatrixRow distanceMatrixRow = distanceMatrix.addLocation(location); planner.addLocation(location, distanceMatrixRow); } @Transactional public synchronized Optional createLocation(Coordinates coordinates, String description) { Objects.requireNonNull(coordinates); Objects.requireNonNull(description); // TODO if (router.isLocationAvailable(coordinates)) Location location = repository.createLocation(coordinates, description); Optional distanceMatrixRow = addToMatrix(location); if (distanceMatrixRow.isPresent()) { planner.addLocation(location, distanceMatrixRow.get()); return Optional.of(location); } else { repository.removeLocation(location.id()); return Optional.empty(); } } private Optional addToMatrix(Location location) { try { DistanceMatrixRow distanceMatrixRow = distanceMatrix.addLocation(location); repository.locations().stream() .filter(existingLocation -> !existingLocation.equals(location)) .forEach(existingLocation -> { distanceRepository.saveDistance(location, existingLocation, distanceMatrixRow.distanceTo(existingLocation.id())); distanceRepository.saveDistance(existingLocation, location, distanceMatrix.distance(existingLocation, location)); }); return Optional.of(distanceMatrixRow); } catch (Exception e) { logger.error( "Failed to calculate distances for location {}, it will be discarded", location.fullDescription(), e); errorEvent.fire(new ErrorEvent( this, "Failed to calculate distances for location " + location.fullDescription() + ", it will be discarded.\n" + e.toString())); return Optional.empty(); } } @Transactional public synchronized void removeLocation(long id) { Optional optionalLocation = repository.find(id); if (!optionalLocation.isPresent()) { errorEvent.fire(new ErrorEvent(this, "Location [" + id + "] cannot be removed because it doesn't exist.")); return; } Location removedLocation = optionalLocation.get(); List locations = repository.locations(); if (locations.size() > 1) { Location depot = locations.stream() .min(comparingLong(Location::id)) .orElseThrow(() -> new IllegalStateException( "Impossible. Locations have size (" + locations.size() + ") but the stream is empty.")); if (removedLocation.equals(depot)) { errorEvent.fire(new ErrorEvent(this, "You can only remove depot if there are no visits.")); return; } } planner.removeLocation(removedLocation); repository.removeLocation(id); distanceMatrix.removeLocation(removedLocation); distanceRepository.deleteDistances(removedLocation); } @Transactional public synchronized void removeAll() { planner.removeAllLocations(); repository.removeAll(); distanceMatrix.clear(); distanceRepository.deleteAll(); } public void populateDistanceMatrix() { repository.locations() .forEach(from -> repository.locations().stream() .filter(to -> !to.equals(from)) .forEach(to -> distanceMatrix.put(from, to, distanceRepository.getDistance(from, to) .orElseThrow(() -> new IllegalStateException("Distance from: [" + from + "] to: [" + to + "] is missing in the distance repository. This should not happen."))))); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/location/package-info.java ================================================ /** * Use cases that involve {@link org.optaweb.vehiclerouting.domain.Location locations}. */ package org.optaweb.vehiclerouting.service.location; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/BoundingBox.java ================================================ package org.optaweb.vehiclerouting.service.region; import java.util.Objects; import org.optaweb.vehiclerouting.domain.Coordinates; /** * Bounding box. */ public class BoundingBox { private final Coordinates southWest; private final Coordinates northEast; /** * Create bounding box. The box must have non-zero dimensions and the corners must be south-west and north-east. * * @param southWest south-west corner (minimal latitude and longitude) * @param northEast north-east corner (maximal latitude and longitude) */ public BoundingBox(Coordinates southWest, Coordinates northEast) { this.southWest = Objects.requireNonNull(southWest); this.northEast = Objects.requireNonNull(northEast); if (southWest.latitude().compareTo(northEast.latitude()) >= 0) { throw new IllegalArgumentException( "South-west corner latitude (" + southWest.latitude() + "N) must be less than north-east corner latitude (" + northEast.latitude() + "N)"); } if (southWest.longitude().compareTo(northEast.longitude()) >= 0) { throw new IllegalArgumentException( "South-west corner longitude (" + southWest.longitude() + "E) must be less than north-east corner longitude (" + northEast.longitude() + "E)"); } } /** * South-west corner of the bounding box. * * @return south-west corner (minimal latitude and longitude) */ public Coordinates getSouthWest() { return southWest; } /** * North-east corner of the bounding box. * * @return north-east corner (maximal latitude and longitude) */ public Coordinates getNorthEast() { return northEast; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/Region.java ================================================ package org.optaweb.vehiclerouting.service.region; public interface Region { BoundingBox getBounds(); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/RegionProperties.java ================================================ package org.optaweb.vehiclerouting.service.region; import java.util.List; import java.util.Optional; import io.smallrye.config.ConfigMapping; @ConfigMapping(prefix = "app.region") public interface RegionProperties { /** * Get country codes specified for the loaded OSM file (working region). * The codes are expected to be in the ISO 3166-1 alpha-2 format. * * @return list of country codes (never {@code null}) */ Optional> countryCodes(); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/RegionService.java ================================================ package org.optaweb.vehiclerouting.service.region; import java.util.List; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; /** * Provides information about the working region. */ @ApplicationScoped public class RegionService { private final RegionProperties regionProperties; private final Region region; @Inject RegionService(RegionProperties regionProperties, Region region) { this.regionProperties = regionProperties; this.region = region; } /** * Country codes matching the working region. * * @return country codes (never {@code null}) */ public List countryCodes() { return regionProperties.countryCodes().orElse(List.of()); } /** * Bounding box of the working region. * * @return bounding box of the working region. */ public BoundingBox boundingBox() { return region.getBounds(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/region/package-info.java ================================================ /** * Provides information about the application's working region. */ package org.optaweb.vehiclerouting.service.region; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/reload/ReloadService.java ================================================ package org.optaweb.vehiclerouting.service.reload; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; import org.optaweb.vehiclerouting.service.location.LocationRepository; import org.optaweb.vehiclerouting.service.location.LocationService; import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; import io.quarkus.runtime.StartupEvent; /** * Reloads data from repositories when the application starts. */ @ApplicationScoped public class ReloadService { private final VehicleRepository vehicleRepository; private final VehicleService vehicleService; private final LocationRepository locationRepository; private final LocationService locationService; @Inject ReloadService( VehicleRepository vehicleRepository, VehicleService vehicleService, LocationRepository locationRepository, LocationService locationService) { this.vehicleRepository = vehicleRepository; this.vehicleService = vehicleService; this.locationRepository = locationRepository; this.locationService = locationService; } public void reload(@Observes StartupEvent startupEvent) { vehicleRepository.vehicles().forEach(vehicleService::addVehicle); locationService.populateDistanceMatrix(); locationRepository.locations().forEach(locationService::addLocation); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/reload/package-info.java ================================================ /** * Loads the application state from repositories when it starts. */ package org.optaweb.vehiclerouting.service.reload; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/RouteChangedEvent.java ================================================ package org.optaweb.vehiclerouting.service.route; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; import org.optaweb.vehiclerouting.domain.Distance; /** * Event published when the routing plan has been updated either by discovering a better route or by a change * in the problem specification (vehicles, visits). */ public class RouteChangedEvent { private final Distance distance; private final List vehicleIds; private final Long depotId; private final List visitIds; private final Collection routes; /** * Create a new ApplicationEvent. * * @param source the object on which the event initially occurred (never {@code null}) * @param distance total distance of all vehicle routes * @param vehicleIds vehicle IDs * @param depotId depot ID (may be {@code null} if there are no locations) * @param visitIds IDs of visits * @param routes vehicle routes */ public RouteChangedEvent( Object source, Distance distance, List vehicleIds, Long depotId, List visitIds, Collection routes) { this.distance = Objects.requireNonNull(distance); this.vehicleIds = Objects.requireNonNull(vehicleIds); this.depotId = depotId; // may be null (no depot) this.visitIds = Objects.requireNonNull(visitIds); this.routes = Objects.requireNonNull(routes); } /** * IDs of all vehicles. * * @return vehicle IDs */ public List vehicleIds() { return vehicleIds; } /** * Routes of all vehicles. * * @return vehicle routes */ public Collection routes() { return routes; } /** * Routing plan distance. * * @return distance (never {@code null}) */ public Distance distance() { return distance; } /** * The depot ID. * * @return depot ID */ public Optional depotId() { return Optional.ofNullable(depotId); } public List visitIds() { return visitIds; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/RouteListener.java ================================================ package org.optaweb.vehiclerouting.service.route; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.inject.Inject; import org.optaweb.vehiclerouting.Profiles; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.Route; import org.optaweb.vehiclerouting.domain.RouteWithTrack; import org.optaweb.vehiclerouting.domain.RoutingPlan; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.service.location.LocationRepository; import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.quarkus.arc.profile.UnlessBuildProfile; /** * Handles route updates emitted by optimization plugin. */ @ApplicationScoped @UnlessBuildProfile(Profiles.TEST) public class RouteListener { private static final Logger logger = LoggerFactory.getLogger(RouteListener.class); private final Router router; private final VehicleRepository vehicleRepository; private final LocationRepository locationRepository; private final Event routingPlanEvent; // TODO maybe remove state from the service and get best route from a repository private RoutingPlan bestRoutingPlan; @Inject RouteListener( Router router, VehicleRepository vehicleRepository, LocationRepository locationRepository, Event routingPlanEvent) { this.router = router; this.vehicleRepository = vehicleRepository; this.locationRepository = locationRepository; this.routingPlanEvent = routingPlanEvent; bestRoutingPlan = RoutingPlan.empty(); } // TODO maybe @ObservesAsync? public void onApplicationEvent(@Observes RouteChangedEvent event) { // TODO persist the best solution Location depot = event.depotId().flatMap(locationRepository::find).orElse(null); try { // TODO Introduce problem revision (every modification increases revision number, event will only // be published if revision numbers match) to avoid looking for missing/extra vehicles/visits. // This will also make it possible to get rid of the try-catch approach. Map vehicleMap = event.vehicleIds().stream() .collect(toMap(vehicleId -> vehicleId, this::findVehicleById)); Map visitMap = event.visitIds().stream() .collect(toMap(visitId -> visitId, this::findLocationById)); List routes = event.routes().stream() // list of deep locations .map(shallowRoute -> new Route( vehicleMap.get(shallowRoute.vehicleId), findLocationById(shallowRoute.depotId), shallowRoute.visitIds.stream() .map(visitMap::get) .collect(toList()))) // add tracks .map(route -> new RouteWithTrack(route, track(route.depot(), route.visits()))) .collect(toList()); bestRoutingPlan = new RoutingPlan( event.distance(), new ArrayList<>(vehicleMap.values()), depot, new ArrayList<>(visitMap.values()), routes); routingPlanEvent.fire(bestRoutingPlan); } catch (IllegalStateException e) { logger.warn("Discarding an outdated routing plan: {}", e.toString()); } } private Vehicle findVehicleById(long id) { return vehicleRepository.find(id).orElseThrow(() -> new IllegalStateException( "Vehicle {id=" + id + "} not found in the repository")); } private Location findLocationById(long id) { return locationRepository.find(id).orElseThrow(() -> new IllegalStateException( "Location {id=" + id + "} not found in the repository")); } private List> track(Location depot, List route) { if (route.isEmpty()) { return Collections.emptyList(); } ArrayList itinerary = new ArrayList<>(); itinerary.add(depot); itinerary.addAll(route); itinerary.add(depot); List> paths = new ArrayList<>(); for (int i = 0; i < itinerary.size() - 1; i++) { Location fromLocation = itinerary.get(i); Location toLocation = itinerary.get(i + 1); List path = router.getPath(fromLocation.coordinates(), toLocation.coordinates()); paths.add(path); } return paths; } public RoutingPlan getBestRoutingPlan() { return bestRoutingPlan; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/Router.java ================================================ package org.optaweb.vehiclerouting.service.route; import java.util.List; import org.optaweb.vehiclerouting.domain.Coordinates; /** * Provides paths between locations. */ public interface Router { /** * Get path between two locations. * * @param from starting location * @param to destination * @return list of coordinates describing the path between given locations. */ List getPath(Coordinates from, Coordinates to); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/ShallowRoute.java ================================================ package org.optaweb.vehiclerouting.service.route; import static java.util.stream.Collectors.joining; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Stream; // TODO maybe remove this once we fork planning domain from optaplanner-examples // because then we can hold a reference to the original location /** * Lightweight route description consisting of vehicle and location IDs instead of entities. * This makes it easier to quickly construct and share result of route optimization * without converting planning domain objects to business domain objects. * Specifically, some information may be lost when converting business domain objects to planning domain * because it's not needed for optimization (e.g. location address) * and so it's impossible to reconstruct the original business object without looking into the repository. */ public class ShallowRoute { /** * Vehicle ID. */ public final long vehicleId; /** * Depot ID. */ public final long depotId; /** * Visit IDs (immutable, never {@code null}). */ public final List visitIds; /** * Create shallow route. * * @param vehicleId vehicle ID * @param depotId depot ID * @param visitIds visit IDs */ public ShallowRoute(long vehicleId, long depotId, List visitIds) { this.vehicleId = vehicleId; this.depotId = depotId; this.visitIds = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(visitIds))); } @Override public String toString() { String route = Stream.concat(Stream.of(depotId), visitIds.stream()) .map(Object::toString) .collect(joining("->", "[", "]")); return vehicleId + ": " + route; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/route/package-info.java ================================================ /** * Handles {@link org.optaweb.vehiclerouting.domain.RoutingPlan route} updates. */ package org.optaweb.vehiclerouting.service.route; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/VehiclePlanner.java ================================================ package org.optaweb.vehiclerouting.service.vehicle; import org.optaweb.vehiclerouting.domain.Vehicle; /** * Optimizes the routing plan in response to vehicle-related changes in the routing problem. */ public interface VehiclePlanner { void addVehicle(Vehicle vehicle); void removeVehicle(Vehicle vehicle); void removeAllVehicles(); void changeCapacity(Vehicle vehicle); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/VehicleRepository.java ================================================ package org.optaweb.vehiclerouting.service.vehicle; import java.util.List; import java.util.Optional; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleData; /** * Defines repository operations on vehicles. */ public interface VehicleRepository { /** * Create a vehicle with a unique ID. * * @param capacity vehicle's capacity * @return a new vehicle */ Vehicle createVehicle(int capacity); /** * Create a vehicle from the given data. * * @param vehicleData vehicle data * @return a new vehicle */ Vehicle createVehicle(VehicleData vehicleData); /** * Get all vehicles. * * @return all vehicles */ List vehicles(); /** * Remove a vehicle with the given ID. * * @param id vehicle's ID * @return the removed vehicle */ Vehicle removeVehicle(long id); /** * Remove all vehicles from the repository. */ void removeAll(); /** * Find a vehicle by its ID. * * @param vehicleId vehicle's ID * @return an Optional containing vehicle with the given ID or empty Optional if there is no vehicle with such ID */ Optional find(long vehicleId); Vehicle changeCapacity(long vehicleId, int capacity); } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/VehicleService.java ================================================ package org.optaweb.vehiclerouting.service.vehicle; import static java.util.Comparator.comparingLong; import java.util.Objects; import java.util.Optional; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.transaction.Transactional; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleData; @ApplicationScoped public class VehicleService { static final int DEFAULT_VEHICLE_CAPACITY = 10; private final VehiclePlanner planner; private final VehicleRepository vehicleRepository; @Inject public VehicleService(VehiclePlanner planner, VehicleRepository vehicleRepository) { this.planner = planner; this.vehicleRepository = vehicleRepository; } @Transactional public Vehicle createVehicle() { Vehicle vehicle = vehicleRepository.createVehicle(DEFAULT_VEHICLE_CAPACITY); addVehicle(vehicle); return vehicle; } @Transactional public Vehicle createVehicle(VehicleData vehicleData) { Vehicle vehicle = vehicleRepository.createVehicle(vehicleData); addVehicle(vehicle); return vehicle; } public void addVehicle(Vehicle vehicle) { planner.addVehicle(Objects.requireNonNull(vehicle)); } @Transactional public void removeVehicle(long vehicleId) { Vehicle vehicle = vehicleRepository.removeVehicle(vehicleId); planner.removeVehicle(vehicle); } public synchronized void removeAnyVehicle() { Optional first = vehicleRepository.vehicles().stream().min(comparingLong(Vehicle::id)); first.map(Vehicle::id).ifPresent(this::removeVehicle); } @Transactional public void removeAll() { planner.removeAllVehicles(); vehicleRepository.removeAll(); } @Transactional public void changeCapacity(long vehicleId, int capacity) { Vehicle updatedVehicle = vehicleRepository.changeCapacity(vehicleId, capacity); planner.changeCapacity(updatedVehicle); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/main/java/org/optaweb/vehiclerouting/service/vehicle/package-info.java ================================================ /** * Performs use cases that involve vehicles. */ package org.optaweb.vehiclerouting.service.vehicle; ================================================ FILE: optaweb-vehicle-routing-backend/src/main/resources/.gitignore ================================================ /application-local.properties ================================================ FILE: optaweb-vehicle-routing-backend/src/main/resources/application.properties ================================================ # App configuration app.demo.data-set-dir=local/dataset app.region.country-codes=BE app.routing.osm-dir=local/openstreetmap app.routing.gh-dir=local/graphhopper app.routing.osm-file=belgium-latest.osm.pbf app.routing.engine=GRAPHHOPPER %test.app.routing.engine=GRAPHHOPPER # OptaPlanner quarkus.optaplanner.solver.daemon=true quarkus.optaplanner.solver.termination.spent-limit=1m # Enable CORS filter. quarkus.http.cors=true # Logging quarkus.log.level=INFO quarkus.log.min-level=DEBUG quarkus.log.category."org.optaweb".level=${LOG_LEVEL_APP:INFO} quarkus.log.category."org.optaplanner".level=${LOG_LEVEL_OPTAPLANNER:INFO} quarkus.log.category."org.drools".level=${LOG_LEVEL_DROOLS:INFO} quarkus.log.category."org.hibernate".level=${LOG_LEVEL_HIBERNATE:INFO} quarkus.log.category."org.jboss.resteasy".level=${LOG_LEVEL_RESTEASY:INFO} # In development mode, the working directory is ./target but local files are expected to survive mvn clean, # so they should be one level above target. %dev.app.demo.data-set-dir=../local/dataset %dev.app.routing.osm-dir=../local/openstreetmap %dev.app.routing.gh-dir=../local/graphhopper ############ # Datasource ############ # [PostgreSQL] - recommended for production %postgresql.quarkus.datasource.db-kind=postgresql %postgresql.quarkus.datasource.jdbc.url=jdbc:postgresql://${DATABASE_HOST:postgresql}:5432/${DATABASE_NAME:optaweb_vrp_database} %postgresql.quarkus.datasource.username=${DATABASE_USER} %postgresql.quarkus.datasource.password=${DATABASE_PASSWORD} %postgresql.quarkus.hibernate-orm.database.generation=update # [Default] - works out of the box, good for development # - using an embedded DB with relative path: http://h2database.com/html/features.html#embedded_databases # - not closing the DB automatically: http://h2database.com/html/features.html#closing_a_database quarkus.datasource.db-kind=h2 quarkus.datasource.jdbc.url=jdbc:h2:file:${app.persistence.h2-dir:../local/db}/${app.persistence.h2-filename:optaweb_vrp_database};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false quarkus.datasource.username=sa quarkus.datasource.password= quarkus.hibernate-orm.database.generation=update # [Tests] # - using an in-memory DB: http://h2database.com/html/features.html#in_memory_databases # - not closing the DB automatically: http://h2database.com/html/features.html#closing_a_database %test.quarkus.datasource.db-kind=h2 %test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE %test.quarkus.datasource.username=sa %test.quarkus.datasource.password= %test.quarkus.hibernate-orm.database.generation=create-drop %test.app.region.country-codes=AT,DE ############### # Cypress tests ############### %test-cypress.app.region.country-codes=DE %test-cypress.app.routing.osm-dir=target-classes/src/test/resources/org/optaweb/vehiclerouting/plugin/routing %test-cypress.app.routing.osm-file=planet_12.032,53.0171_12.1024,53.0491.osm.pbf %test-cypress.optaplanner.solver.termination.spent-limit=10s %test-cypress.quarkus.datasource.db-kind=h2 %test-cypress.quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE %test-cypress.quarkus.datasource.username=sa %test-cypress.quarkus.datasource.password= ================================================ FILE: optaweb-vehicle-routing-backend/src/main/resources/org/optaweb/vehiclerouting/service/demo/belgium-cities.yaml ================================================ --- name: "Belgium cities" vehicles: - name: "Vehicle 1" capacity: 10 - name: "Vehicle 2" capacity: 10 - name: "Vehicle 3" capacity: 10 - name: "Vehicle 4" capacity: 10 - name: "Vehicle 5" capacity: 10 - name: "Vehicle 6" capacity: 10 depot: label: "Brussels" lat: 50.85 lng: 4.35 visits: - label: "Aalst" lat: 50.933333 lng: 4.033333 - label: "Anderlecht" lat: 50.833333 lng: 4.333333 - label: "Antwerp" lat: 51.217778 lng: 4.400278 - label: "Beringen" lat: 51.033333 lng: 5.216667 - label: "Bruges" lat: 51.216667 lng: 3.233333 - label: "Charleroi" lat: 50.4 lng: 4.433333 - label: "Chatelet" lat: 50.4 lng: 4.516667 - label: "Dendermonde" lat: 51.033333 lng: 4.1 - label: "Geel" lat: 51.166667 lng: 5.0 - label: "Genk" lat: 50.966667 lng: 5.5 - label: "Ghent" lat: 51.05 lng: 3.733333 - label: "Halle" lat: 50.733333 lng: 4.233333 - label: "Hasselt" lat: 50.93 lng: 5.34 - label: "Ixelles" lat: 50.833333 lng: 4.366667 - label: "Kortrijk" lat: 50.833333 lng: 3.266667 - label: "La Louviere" lat: 50.466667 lng: 4.183333 - label: "Leuven" lat: 50.883333 lng: 4.7 - label: "Liege" lat: 50.633333 lng: 5.566667 - label: "Lier" lat: 51.133333 lng: 4.566667 - label: "Lokeren" lat: 51.1 lng: 3.983333 - label: "Mechelen" lat: 51.016667 lng: 4.466667 - label: "Molenbeek" lat: 50.857778 lng: 4.315833 - label: "Mons" lat: 50.45 lng: 3.95 - label: "Namur" lat: 50.466667 lng: 4.866667 - label: "Ninove" lat: 50.833333 lng: 4.016667 - label: "Roeselare" lat: 50.933333 lng: 3.116667 - label: "Schaerbeek" lat: 50.866667 lng: 4.383333 - label: "Seraing" lat: 50.583333 lng: 5.5 - label: "Sint Niklaas" lat: 51.166667 lng: 4.133333 - label: "Sint Truiden" lat: 50.8 lng: 5.183333 - label: "Tienen" lat: 50.8 lng: 4.933333 - label: "Tournai" lat: 50.6 lng: 3.383333 - label: "Turnhout" lat: 51.316667 lng: 4.95 - label: "Uccle" lat: 50.8 lng: 4.333333 - label: "Verviers" lat: 50.583333 lng: 5.85 - label: "Vilvoorde" lat: 50.933333 lng: 4.416667 - label: "Waregem" lat: 50.883333 lng: 3.416667 - label: "Wavre" lat: 50.716667 lng: 4.6 - label: "Ypres" lat: 50.85 lng: 2.883333 ================================================ FILE: optaweb-vehicle-routing-backend/src/main/resources/solverConfig.xml ================================================ org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution org.optaweb.vehiclerouting.plugin.planner.domain.Standstill org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit org.optaweb.vehiclerouting.plugin.planner.VehicleRoutingConstraintProvider ONLY_DOWN FIRST_FIT_DECREASING true true 200 1 ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/TestConfig.java ================================================ package org.optaweb.vehiclerouting; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import org.mockito.Mockito; import org.optaweb.vehiclerouting.service.route.RouteListener; import com.graphhopper.GraphHopper; import io.quarkus.arc.profile.IfBuildProfile; import io.quarkus.test.junit.QuarkusTest; @Dependent public class TestConfig { /** * Creates a GraphHopper mock that may be used when running a {@link QuarkusTest @QuarkusTest}. * * @return mock GraphHopper */ @IfBuildProfile(Profiles.TEST) @Produces public GraphHopper graphHopper() { return Mockito.mock(GraphHopper.class); } /** * Creates a mock route listener to avoid things like touching database and WebSocket. * * @return mock RouteListener */ @IfBuildProfile(Profiles.TEST) @Produces public RouteListener routeListener() { return Mockito.mock(RouteListener.class); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/CoordinatesTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.math.BigDecimal; import org.junit.jupiter.api.Test; class CoordinatesTest { @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new Coordinates(null, BigDecimal.ZERO)); assertThatNullPointerException().isThrownBy(() -> new Coordinates(BigDecimal.ZERO, null)); } @Test void coordinates_should_be_equals_when_numerically_equal() { Coordinates coordinates = new Coordinates(BigDecimal.valueOf(987.1234), BigDecimal.valueOf(-0.1111)); assertThat(coordinates).isEqualTo(coordinates); BigDecimal ONE_POINT_ZERO = new BigDecimal("1.0"); BigDecimal MINUS_ZERO = BigDecimal.ZERO.negate(); Coordinates coordinates01 = new Coordinates(BigDecimal.ZERO, BigDecimal.ONE); assertThat(new Coordinates(MINUS_ZERO, ONE_POINT_ZERO)) .isEqualTo(coordinates01) .hasSameHashCodeAs(coordinates01); Coordinates coordinates10 = new Coordinates(BigDecimal.ONE, BigDecimal.ZERO); assertThat(new Coordinates(ONE_POINT_ZERO, MINUS_ZERO)) .isEqualTo(coordinates10) .hasSameHashCodeAs(coordinates10); } @Test void should_not_equal() { assertThat(new Coordinates(BigDecimal.ONE, BigDecimal.TEN)) .isNotEqualTo(null) .isNotEqualTo(BigDecimal.valueOf(11)) .isNotEqualTo(new Coordinates(BigDecimal.ONE, BigDecimal.ONE)) .isNotEqualTo(new Coordinates(BigDecimal.TEN, BigDecimal.TEN)); } @Test void valueOf_and_getters() { double latitude = Math.E; double longitude = Math.PI; Coordinates coordinates = Coordinates.of(latitude, longitude); assertThat(coordinates.latitude()).isEqualTo(BigDecimal.valueOf(latitude)); assertThat(coordinates.longitude()).isEqualTo(BigDecimal.valueOf(longitude)); } @Test void toString_should_contain_latitude_and_longitude() { String pi = "3.14159265358979323846"; assertThat(new Coordinates(BigDecimal.ONE, new BigDecimal(pi))).hasToString("[1, " + pi + "]"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/CountryCodeValidatorTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.optaweb.vehiclerouting.domain.CountryCodeValidator.validate; import java.util.ArrayList; import java.util.Arrays; import org.junit.jupiter.api.Test; class CountryCodeValidatorTest { @Test void should_fail_on_invalid_country_codes() { assertThatNullPointerException().isThrownBy(() -> validate(null)); assertThatNullPointerException().isThrownBy(() -> validate(Arrays.asList("US", null, "CA"))); assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("XX"))); assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("CZE"))); assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("D"))); assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList(""))); assertThatIllegalArgumentException().isThrownBy(() -> validate(Arrays.asList("US", "XY", "CA"))); } @Test void should_ignore_case_and_convert_to_upper_case() { assertThat(validate(Arrays.asList("us"))).containsExactly("US"); } @Test void should_allow_multiple_values() { assertThat(validate(Arrays.asList("US", "ca"))).containsExactly("US", "CA"); } @Test void should_allow_empty_list() { assertThat(validate(new ArrayList<>())).isEmpty(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/DistanceTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import org.junit.jupiter.api.Test; class DistanceTest { @Test void distance_millis_should_be_same_as_the_given_value() { long millis = 123_999; assertThat(Distance.ofMillis(millis).millis()).isEqualTo(millis); } @Test void toString_should_contain_units_and_be_human_readable() { assertThat(Distance.ofMillis(3600_000 * 37 + 60_000 * 3 + 24_000)).hasToString("37h 3m 24s 0ms"); assertThat(Distance.ofMillis(3601_000)).hasToString("1h 0m 1s 0ms"); assertThat(Distance.ofMillis(5_123)).hasToString("0h 0m 5s 123ms"); } @Test void time_must_be_positive_or_zero() { assertThatIllegalArgumentException().isThrownBy(() -> Distance.ofMillis(-1)).withMessageContaining("(-1)"); assertThatCode(() -> Distance.ofMillis(0)).doesNotThrowAnyException(); } @Test void equals_hashCode() { long millis = 37; Distance distance = Distance.ofMillis(millis); assertThat(distance) .isEqualTo(distance) .isEqualTo(Distance.ofMillis(millis)) .isNotEqualTo(null) .isNotEqualTo(millis) .isNotEqualTo(Distance.ofMillis(millis + 1)) .hasSameHashCodeAs(Distance.ofMillis(millis)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/LocationDataTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.math.BigDecimal; import org.junit.jupiter.api.Test; class LocationDataTest { @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new LocationData(null, "")); assertThatNullPointerException().isThrownBy(() -> new LocationData(Coordinates.of(1, 1), null)); } @Test void locations_are_equal_if_they_have_same_properties() { Coordinates coordinates0 = new Coordinates(BigDecimal.ZERO, BigDecimal.ZERO); Coordinates coordinates1 = new Coordinates(BigDecimal.ONE, BigDecimal.ONE); String description = "test description"; LocationData equalLocationData = new LocationData(coordinates0, description); final LocationData locationData = new LocationData(coordinates0, description); assertThat(locationData) // different coordinates .isNotEqualTo(new LocationData(coordinates1, description)) // different description .isNotEqualTo(new LocationData(coordinates0, "xyz")) // null .isNotEqualTo(null) // different type with equal properties .isNotEqualTo(new Location(0, coordinates0, description)) // same object -> OK .isEqualTo(locationData) // same properties -> OK .isEqualTo(equalLocationData) // equal objects => same hashCode .hasSameHashCodeAs(equalLocationData); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/LocationTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.math.BigDecimal; import org.junit.jupiter.api.Test; class LocationTest { @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new Location(0, null, "")); assertThatNullPointerException().isThrownBy(() -> new Location(0, Coordinates.of(1, 1), null)); } @Test void locations_are_identified_based_on_id() { final Coordinates coordinates0 = new Coordinates(BigDecimal.ZERO, BigDecimal.ZERO); final Coordinates coordinates1 = new Coordinates(BigDecimal.ONE, BigDecimal.ONE); final String description = "test description"; final long id = 0; final Location location = new Location(id, coordinates0, description); assertThat(location) // different ID .isNotEqualTo(new Location(1, coordinates0, description)) // null .isNotEqualTo(null) // different class .isNotEqualTo(new LocationData(coordinates0, description)) // same object -> OK .isEqualTo(location) // same properties -> OK .isEqualTo(new Location(id, coordinates0, description)) // same ID, different coordinate -> OK .isEqualTo(new Location(id, coordinates1, description)) // same ID, different description -> OK .isEqualTo(new Location(id, coordinates0, "xyz")); } @Test void equal_locations_must_have_same_hashcode() { long id = 1; assertThat(new Location(id, Coordinates.of(1, 1), "description 1")) .hasSameHashCodeAs(new Location(id, Coordinates.of(2, 2), "description 2")); } @Test void constructor_without_description_should_create_empty_description() { assertThat(new Location(7, Coordinates.of(3.14, 4.13)).description()).isEmpty(); } @Test void toString_should_contain_id_and_description() { Coordinates coordinates = Coordinates.of(1, 1); assertThat(new Location(7, coordinates, "")).hasToString("7"); assertThat(new Location(5, coordinates, "home")).hasToString("5: 'home'"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/RouteTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; class RouteTest { private final Vehicle vehicle = VehicleFactory.testVehicle(4); private final Location depot = new Location(1, Coordinates.of(5, 5)); private final Location visit1 = new Location(2, Coordinates.of(5, 5)); private final Location visit2 = new Location(3, Coordinates.of(5, 5)); @Test void constructor_args_not_null() { assertThatNullPointerException().isThrownBy(() -> new Route(null, depot, Collections.emptyList())); assertThatNullPointerException().isThrownBy(() -> new Route(vehicle, null, Collections.emptyList())); assertThatNullPointerException().isThrownBy(() -> new Route(vehicle, depot, null)); } @Test void visits_should_not_contain_depot() { assertThatIllegalArgumentException() .isThrownBy(() -> new Route(vehicle, depot, Arrays.asList(depot, visit1))) .withMessageContaining(depot.toString()); assertThatIllegalArgumentException() .isThrownBy(() -> new Route(vehicle, depot, Arrays.asList(visit1, depot, visit2))) .withMessageContaining(depot.toString()); } @Test void no_visit_should_be_visited_twice_by_the_same_vehicle() { assertThatIllegalArgumentException() .isThrownBy(() -> new Route(vehicle, depot, Arrays.asList(visit1, visit1))) .withMessageContaining("(1)"); } @Test void cannot_modify_visits_externally() { ArrayList visits = new ArrayList<>(); visits.add(visit1); List routeVisits = new Route(vehicle, depot, visits).visits(); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(routeVisits::clear); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/RouteWithTrackTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; class RouteWithTrackTest { private final Vehicle vehicle = VehicleFactory.testVehicle(4); private final Location depot = new Location(1, Coordinates.of(5, 5)); private final Location visit1 = new Location(2, Coordinates.of(5, 5)); private final Location visit2 = new Location(3, Coordinates.of(5, 5)); @Test void constructor_args_not_null() { Route route = new Route(vehicle, depot, emptyList()); assertThatNullPointerException().isThrownBy(() -> new RouteWithTrack(route, null)); assertThatNullPointerException().isThrownBy(() -> new RouteWithTrack(null, emptyList())); } @Test void cannot_modify_track_externally() { Route route = new Route(vehicle, depot, Arrays.asList(visit1, visit2)); ArrayList> track = new ArrayList<>(); track.add(Arrays.asList(Coordinates.of(1.0, 2.0))); List> routeTrack = new RouteWithTrack(route, track).track(); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(routeTrack::clear); } @Test void when_route_is_empty_track_must_be_empty() { Route emptyRoute = new Route(vehicle, depot, emptyList()); ArrayList> track = new ArrayList<>(); track.add(Arrays.asList(Coordinates.of(1.0, 2.0))); assertThatIllegalArgumentException().isThrownBy(() -> new RouteWithTrack(emptyRoute, track)); } @Test void when_route_is_nonempty_track_must_be_nonempty() { Route route = new Route(vehicle, depot, Arrays.asList(visit1, visit2)); ArrayList> emptyTrack = new ArrayList<>(); assertThatIllegalArgumentException().isThrownBy(() -> new RouteWithTrack(route, emptyTrack)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/RoutingPlanTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; class RoutingPlanTest { private final Distance distance = Distance.ofMillis(1); private final Vehicle vehicle = VehicleFactory.testVehicle(1); private final List vehicles = singletonList(vehicle); private final Location depot = new Location(1, Coordinates.of(5, 5)); private final Location visit = new Location(2, Coordinates.of(3, 3)); private final RouteWithTrack emptyRoute = new RouteWithTrack(new Route(vehicle, depot, emptyList()), emptyList()); // Track is not important (we don't check if track starts and ends in the depot and goes through all visits) private final List> nonEmptyTrack = singletonList(singletonList(Coordinates.of(5, 5))); @Test void constructor_args_not_null() { assertThatNullPointerException() .isThrownBy(() -> new RoutingPlan(null, vehicles, depot, emptyList(), emptyList())); assertThatNullPointerException() .isThrownBy(() -> new RoutingPlan(distance, null, depot, emptyList(), emptyList())); assertThatNullPointerException() .isThrownBy(() -> new RoutingPlan(distance, vehicles, depot, null, emptyList())); assertThatNullPointerException() .isThrownBy(() -> new RoutingPlan(distance, vehicles, depot, emptyList(), null)); // depot can be null // TODO create a factory that will prevent passing a null depot accidentally } @Test void no_visits_without_a_depot() { List> track = singletonList(singletonList(visit.coordinates())); RouteWithTrack routeWithTrack = new RouteWithTrack(new Route(vehicle, depot, singletonList(visit)), track); assertThatIllegalArgumentException().isThrownBy(() -> new RoutingPlan( distance, vehicles, null, singletonList(visit), singletonList(routeWithTrack))); } @Test void no_routes_without_a_depot() { assertThatIllegalArgumentException() .isThrownBy(() -> new RoutingPlan(distance, vehicles, null, emptyList(), singletonList(emptyRoute))); } @Test void there_must_be_one_route_per_vehicle_when_there_is_a_depot() { assertThatIllegalArgumentException() .isThrownBy(() -> new RoutingPlan(distance, vehicles, depot, emptyList(), emptyList())) .withMessageContaining("Vehicles (1): [") .withMessageContaining("Routes' vehicleIds (0): []"); } @Test void routes_vehicle_references_must_be_consistent_with_vehicles_in_routing_plan() { List unexpectedVehicles = singletonList(VehicleFactory.testVehicle(vehicle.id() + 1)); List routes = singletonList(emptyRoute); assertThatIllegalArgumentException() .isThrownBy(() -> new RoutingPlan(distance, unexpectedVehicles, depot, emptyList(), routes)) .withMessageContaining("Vehicles (1): [") .withMessageContaining("Routes' vehicleIds (1): [" + vehicle.id() + "]"); } @Test void routes_visit_references_must_be_consistent_with_visits_in_routing_plan() { Vehicle vehicle1 = VehicleFactory.testVehicle(1); Vehicle vehicle2 = VehicleFactory.testVehicle(2); Location depot = new Location(100, Coordinates.of(0, 0), "depot"); Location visit1 = new Location(101, Coordinates.of(1, 1), "visit1"); Location visit2 = new Location(102, Coordinates.of(2, 2), "visit2"); Location visit3 = new Location(103, Coordinates.of(3, 3), "visit3"); assertThatCode(() -> new RoutingPlan( distance, emptyList(), // no vehicles depot, singletonList(visit1), emptyList() // => no routes (no visits visited) )).doesNotThrowAnyException(); assertThatIllegalArgumentException().isThrownBy(() -> new RoutingPlan( distance, singletonList(vehicle1), depot, asList(visit1, visit2), singletonList( // visit3 extra new RouteWithTrack(new Route(vehicle1, depot, asList(visit1, visit2, visit3)), nonEmptyTrack)))) .withMessageContaining(visit3.toString()); Location visit4 = new Location(104, Coordinates.of(4, 4), "visit4"); assertThatIllegalArgumentException().isThrownBy(() -> new RoutingPlan( distance, asList(vehicle1, vehicle2), depot, asList(visit1, visit2, visit3), // visit3 missing, visit4 extra asList( new RouteWithTrack(new Route(vehicle1, depot, asList(visit1, visit4)), nonEmptyTrack), new RouteWithTrack(new Route(vehicle2, depot, singletonList(visit2)), nonEmptyTrack)))) .withMessageContaining(visit4.toString()); } @Test void cannot_modify_collections_externally() { // Use modifiable collections as the input ArrayList vehicles = new ArrayList<>(); ArrayList visits = new ArrayList<>(); ArrayList routes = new ArrayList<>(); vehicles.add(vehicle); visits.add(visit); routes.add(new RouteWithTrack(new Route(vehicle, depot, singletonList(visit)), nonEmptyTrack)); RoutingPlan routingPlan = new RoutingPlan(distance, vehicles, depot, visits, routes); List planVehicles = routingPlan.vehicles(); List planVisits = routingPlan.visits(); List planRoutes = routingPlan.routes(); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(planVehicles::clear); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(planVisits::clear); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(planRoutes::clear); } @Test void empty_routing_plan_should_be_empty() { RoutingPlan empty = RoutingPlan.empty(); assertThat(empty.distance()).isEqualTo(Distance.ZERO); assertThat(empty.vehicles()).isEmpty(); assertThat(empty.depot()).isEmpty(); assertThat(empty.visits()).isEmpty(); assertThat(empty.routes()).isEmpty(); } @Test void isEmpty() { assertThat(RoutingPlan.empty().isEmpty()).isTrue(); assertThat(new RoutingPlan(Distance.ZERO, emptyList(), depot, emptyList(), emptyList()).isEmpty()).isFalse(); assertThat(new RoutingPlan(Distance.ZERO, vehicles, null, emptyList(), emptyList()).isEmpty()).isFalse(); assertThat(new RoutingPlan(Distance.ZERO, vehicles, depot, emptyList(), singletonList(emptyRoute)).isEmpty()) .isFalse(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/VehicleDataTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.Test; class VehicleDataTest { @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new VehicleData(null, 1)); } @Test void vehicles_are_equal_if_they_have_same_properties() { String name = "vehicle name"; int capacity = 20; VehicleData vehicleData = new VehicleData(name, capacity); assertThat(vehicleData) // different name .isNotEqualTo(new VehicleData("", capacity)) // different capacity .isNotEqualTo(new VehicleData(name, capacity + 1)) // null .isNotEqualTo(null) // different type with equal properties .isNotEqualTo(new Vehicle(0, name, capacity)) // same object -> OK .isEqualTo(vehicleData) // same properties -> OK .isEqualTo(new VehicleData(name, capacity)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/VehicleFactoryTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; class VehicleFactoryTest { @Test void createVehicle() { long vehicleId = 4; String name = "Vehicle four"; int capacity = 99; Vehicle vehicle = VehicleFactory.createVehicle(vehicleId, name, capacity); assertThat(vehicle.id()).isEqualTo(vehicleId); assertThat(vehicle.name()).isEqualTo(name); assertThat(vehicle.capacity()).isEqualTo(capacity); } @Test void vehicleData() { String name = "vehicle name"; int capacity = 1000; VehicleData vehicleData = VehicleFactory.vehicleData(name, capacity); assertThat(vehicleData.name()).isEqualTo(name); assertThat(vehicleData.capacity()).isEqualTo(capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/domain/VehicleTest.java ================================================ package org.optaweb.vehiclerouting.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.Test; class VehicleTest { @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new Vehicle(0, null, 0)); } @Test void vehicles_are_identified_based_on_id() { final long id = 0; final String description = "test description"; final int capacity = 1; final Vehicle vehicle = new Vehicle(id, description, capacity); assertThat(vehicle) // different ID .isNotEqualTo(new Vehicle(id + 1, description, capacity)) // null .isNotEqualTo(null) // different class .isNotEqualTo(id) // same object -> OK .isEqualTo(vehicle) // same properties -> OK .isEqualTo(new Vehicle(id, description, capacity)) // same ID, different description -> OK .isEqualTo(new Vehicle(id, description + "x", capacity)) // same ID, different capacity -> OK .isEqualTo(new Vehicle(id, description, capacity + 1)); } @Test void equal_vehicles_must_have_same_hashcode() { long id = 1; assertThat(new Vehicle(id, "description 1", 1)) .hasSameHashCodeAs(new Vehicle(id, "description 2", 2)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceEntityTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.Test; class DistanceEntityTest { @Test void constructor_params_must_not_be_null() { DistanceKey dKey = new DistanceKey(1, 2); assertThatNullPointerException().isThrownBy(() -> new DistanceEntity(null, 100L)); assertThatNullPointerException().isThrownBy(() -> new DistanceEntity(dKey, null)); } @Test void equals() { final long from = 10; final long to = 2000; final DistanceKey distanceKey = new DistanceKey(from, to); final long distance = 50001; DistanceEntity distanceEntity = new DistanceEntity(distanceKey, distance); assertThat(distanceEntity) .isEqualTo(distanceEntity) .isEqualTo(new DistanceEntity(new DistanceKey(from, to), distance)) .isNotEqualTo(null) .isNotEqualTo(distanceKey) .isNotEqualTo(new DistanceEntity(distanceKey, distance + 1)) .isNotEqualTo(new DistanceEntity(new DistanceKey(to, from), distance)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceRepositoryImplTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; @ExtendWith(MockitoExtension.class) class DistanceRepositoryImplTest { @Mock private DistanceCrudRepository crudRepository; @InjectMocks private DistanceRepositoryImpl repository; @Captor private ArgumentCaptor distanceEntityArgumentCaptor; private final Location from = new Location(1, Coordinates.of(7, -4.0)); private final Location to = new Location(2, Coordinates.of(5, 9.0)); @Test void should_save_distance() { long distance = 956766417; repository.saveDistance(from, to, Distance.ofMillis(distance)); verify(crudRepository).persist(distanceEntityArgumentCaptor.capture()); DistanceEntity distanceEntity = distanceEntityArgumentCaptor.getValue(); assertThat(distanceEntity.getDistance()).isEqualTo(distance); assertThat(distanceEntity.getKey().getFromId()).isEqualTo(from.id()); assertThat(distanceEntity.getKey().getToId()).isEqualTo(to.id()); } @Test void should_return_distance_when_entity_is_found() { DistanceKey distanceKey = new DistanceKey(from.id(), to.id()); long distance = 10305; DistanceEntity distanceEntity = new DistanceEntity(distanceKey, distance); when(crudRepository.findByIdOptional(distanceKey)).thenReturn(Optional.of(distanceEntity)); assertThat(repository.getDistance(from, to)).contains(Distance.ofMillis(distance)); } @Test void should_return_negative_number_when_distance_not_found() { when(crudRepository.findByIdOptional(any(DistanceKey.class))).thenReturn(Optional.empty()); assertThat(repository.getDistance(from, to)).isEmpty(); } @Test void should_delete_distance_by_location_id() { repository.deleteDistances(from); verify(crudRepository).deleteByFromIdOrToId(from.id()); } @Test void should_delete_all_distances() { repository.deleteAll(); verify(crudRepository).deleteAll(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/DistanceRepositoryIntegrationTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import io.quarkus.test.TestTransaction; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest class DistanceRepositoryIntegrationTest { @Inject DistanceCrudRepository crudRepository; private DistanceRepositoryImpl repository; @BeforeEach void setUp() { repository = new DistanceRepositoryImpl(crudRepository); } @Test @TestTransaction void panache_repository_should_persist_and_delete_distances() { DistanceKey key = new DistanceKey(1, 2); DistanceEntity entity = new DistanceEntity(key, 730107L); crudRepository.persist(entity); assertThat(crudRepository.count()).isOne(); assertThat(crudRepository.findById(key)).isEqualTo(entity); crudRepository.deleteById(key); assertThat(crudRepository.count()).isZero(); } static DistanceEntity distance(long fromId, long toId) { return new DistanceEntity(new DistanceKey(fromId, toId), 1L); } @Test @TestTransaction void delete_by_fromId_or_toId() { DistanceEntity distance23 = distance(2, 3); DistanceEntity distance32 = distance(3, 2); crudRepository.persist(distance(1, 2)); crudRepository.persist(distance(2, 1)); crudRepository.persist(distance23); crudRepository.persist(distance32); crudRepository.persist(distance(3, 1)); crudRepository.persist(distance(1, 3)); assertThat(crudRepository.count()).isEqualTo(6); crudRepository.deleteByFromIdOrToId(1L); assertThat(crudRepository.count()).isEqualTo(2); assertThat(crudRepository.findAll().stream()).containsExactly(distance23, distance32); } @Test @TestTransaction void should_return_saved_distance() { Location location1 = new Location(1, Coordinates.of(7, -4.0)); Location location2 = new Location(2, Coordinates.of(5, 9.0)); Distance distance = Distance.ofMillis(956766417); repository.saveDistance(location1, location2, distance); assertThat(repository.getDistance(location1, location2)).contains(distance); } @Test void should_return_negative_number_when_distance_not_found() { Location location1 = new Location(1, Coordinates.of(7, -4.0)); Location location2 = new Location(2, Coordinates.of(5, 9.0)); assertThat(repository.getDistance(location1, location2)).isEmpty(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/LocationEntityTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.math.BigDecimal; import org.junit.jupiter.api.Test; class LocationEntityTest { @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new LocationEntity(0, null, BigDecimal.ZERO, "")); assertThatNullPointerException().isThrownBy(() -> new LocationEntity(0, BigDecimal.ZERO, null, "")); assertThatNullPointerException().isThrownBy(() -> new LocationEntity(0, BigDecimal.ZERO, BigDecimal.ONE, null)); } @Test void getters() { int id = 10; BigDecimal latitude = BigDecimal.valueOf(0.101); BigDecimal longitude = BigDecimal.valueOf(101.0); String description = "Description."; LocationEntity locationEntity = new LocationEntity(id, latitude, longitude, description); assertThat(locationEntity.getId()).isEqualTo(id); assertThat(locationEntity.getLongitude()).isEqualTo(longitude); assertThat(locationEntity.getLatitude()).isEqualTo(latitude); assertThat(locationEntity.getDescription()).isEqualTo(description); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/LocationRepositoryImplTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; @ExtendWith(MockitoExtension.class) class LocationRepositoryImplTest { @Mock private LocationCrudRepository crudRepository; @InjectMocks private LocationRepositoryImpl repository; @Captor private ArgumentCaptor locationEntityCaptor; private final Location testLocation = new Location(76, Coordinates.of(1.2, 3.4), "description"); private static LocationEntity locationEntity(Location location) { return new LocationEntity( location.id(), location.coordinates().latitude(), location.coordinates().longitude(), location.description()); } @Test void should_create_location() { // arrange Coordinates savedCoordinates = Coordinates.of(0.00213, 32.777); String savedDescription = "new location"; // act Location newLocation = repository.createLocation(savedCoordinates, savedDescription); // assert // -- the correct values were used to save the entity verify(crudRepository).persist(locationEntityCaptor.capture()); LocationEntity savedLocation = locationEntityCaptor.getValue(); assertThat(savedLocation.getLatitude()).isEqualTo(savedCoordinates.latitude()); assertThat(savedLocation.getLongitude()).isEqualTo(savedCoordinates.longitude()); assertThat(savedLocation.getDescription()).isEqualTo(savedDescription); // -- created domain location has the expected values assertThat(newLocation.coordinates()).isEqualTo(savedCoordinates); assertThat(newLocation.description()).isEqualTo(savedDescription); } @Test void remove_created_location_by_id() { LocationEntity locationEntity = locationEntity(testLocation); final long id = testLocation.id(); when(crudRepository.findByIdOptional(id)).thenReturn(Optional.of(locationEntity)); Location removed = repository.removeLocation(id); assertThat(removed).isEqualTo(testLocation); verify(crudRepository).deleteById(id); } @Test void removing_nonexistent_location_should_fail() { when(crudRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty()); // removing nonexistent location should fail and its ID should appear in the exception message int uniqueNonexistentId = 7173; assertThatIllegalArgumentException() .isThrownBy(() -> repository.removeLocation(uniqueNonexistentId)) .withMessageContaining(String.valueOf(uniqueNonexistentId)); } @Test void remove_all_locations() { repository.removeAll(); verify(crudRepository).deleteAll(); } @Test void get_all_locations() { LocationEntity locationEntity = locationEntity(testLocation); when(crudRepository.streamAll()).thenReturn(Stream.of(locationEntity)); assertThat(repository.locations()).containsExactly(testLocation); } @Test void find_by_id() { LocationEntity locationEntity = locationEntity(testLocation); when(crudRepository.findByIdOptional(testLocation.id())).thenReturn(Optional.of(locationEntity)); assertThat(repository.find(testLocation.id())).contains(testLocation); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/LocationRepositoryIntegrationTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import java.math.BigDecimal; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import io.quarkus.test.TestTransaction; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest class LocationRepositoryIntegrationTest { @Inject LocationCrudRepository crudRepository; private LocationRepositoryImpl repository; @BeforeEach void setUp() { repository = new LocationRepositoryImpl(crudRepository); } @Test @TestTransaction void db_schema() { // https://wiki.openstreetmap.org/wiki/Node#Structure final BigDecimal maxLatitude = new BigDecimal("90.0000000"); final BigDecimal maxLongitude = new BigDecimal("214.7483647"); final BigDecimal minLatitude = maxLatitude.negate(); final BigDecimal minLongitude = maxLongitude.negate(); final String description = "..."; LocationEntity minLocation = new LocationEntity(0, minLatitude, minLongitude, description); LocationEntity maxLocation = new LocationEntity(0, maxLatitude, maxLongitude, description); crudRepository.persist(minLocation); crudRepository.persist(maxLocation); assertThat(minLocation.getId()).isNotZero(); assertThat(maxLocation.getId()).isNotZero(); assertThat(crudRepository.findById(minLocation.getId())).isEqualTo(minLocation); assertThat(crudRepository.findById(maxLocation.getId())).isEqualTo(maxLocation); } @Test @TestTransaction void remove_created_location() { Coordinates coordinates = Coordinates.of(0.00213, 32.777); assertThat(crudRepository.count()).isZero(); Location location = repository.createLocation(coordinates, ""); assertThat(location.coordinates()).isEqualTo(coordinates); assertThat(crudRepository.count()).isOne(); Location removed = repository.removeLocation(location.id()); assertThat(removed).isEqualTo(location); // removing the same location twice should fail assertThatIllegalArgumentException().isThrownBy(() -> repository.removeLocation(location.id())); // removing nonexistent location should fail and its ID should appear in the exception message int uniqueNonexistentId = 7173; assertThatIllegalArgumentException().isThrownBy(() -> repository.removeLocation(uniqueNonexistentId)) .withMessageContaining(String.valueOf(uniqueNonexistentId)); } @Test @TestTransaction void get_and_remove_all_locations() { int locationCount = 8; for (int i = 0; i < locationCount; i++) { repository.createLocation(Coordinates.of(1.0, i / 100.0), ""); } assertThat(crudRepository.count()).isEqualTo(locationCount); // get a sample location entity from the repository LocationEntity testEntity = crudRepository .findByIdOptional((long) locationCount) .orElseThrow(IllegalStateException::new); Location testLocation = new Location( testEntity.getId(), new Coordinates(testEntity.getLatitude(), testEntity.getLongitude())); assertThat(repository.locations()) .hasSize(locationCount) .contains(testLocation); repository.removeAll(); assertThat(crudRepository.count()).isZero(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleEntityTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; class VehicleEntityTest { @Test void getters() { long id = 321; String name = "Vehicle XY"; int capacity = 11; VehicleEntity vehicleEntity = new VehicleEntity(id, name, capacity); assertThat(vehicleEntity.getId()).isEqualTo(id); assertThat(vehicleEntity.getName()).isEqualTo(name); assertThat(vehicleEntity.getCapacity()).isEqualTo(capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/persistence/VehicleRepositoryImplTest.java ================================================ package org.optaweb.vehiclerouting.plugin.persistence; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleData; import org.optaweb.vehiclerouting.domain.VehicleFactory; @ExtendWith(MockitoExtension.class) class VehicleRepositoryImplTest { @Mock private VehicleCrudRepository crudRepository; @InjectMocks private VehicleRepositoryImpl repository; @Captor private ArgumentCaptor vehicleEntityCaptor; private final Vehicle testVehicle = VehicleFactory.createVehicle(19, "vehicle name", 1100); private static VehicleEntity vehicleEntity(Vehicle vehicle) { return new VehicleEntity(vehicle.id(), vehicle.name(), vehicle.capacity()); } @Test void should_create_vehicle() { // arrange int savedCapacity = 1; // act Vehicle newVehicle = repository.createVehicle(savedCapacity); // assert // -- the correct values were used to save the entity verify(crudRepository).persist(vehicleEntityCaptor.capture()); VehicleEntity savedVehicle = vehicleEntityCaptor.getValue(); assertThat(savedVehicle.getName()).isNotNull(); assertThat(savedVehicle.getCapacity()).isEqualTo(savedCapacity); // -- created domain vehicle has the expected values assertThat(newVehicle.name()).isNotNull(); assertThat(newVehicle.capacity()).isEqualTo(savedCapacity); } @Test void create_vehicle_from_given_data() { // arrange String name = "x"; int capacity = 111; VehicleData vehicleData = VehicleFactory.vehicleData(name, capacity); // act Vehicle newVehicle = repository.createVehicle(vehicleData); // assert assertThat(newVehicle.name()).isEqualTo(name); assertThat(newVehicle.capacity()).isEqualTo(capacity); } @Test void remove_created_vehicle_by_id() { VehicleEntity vehicleEntity = vehicleEntity(testVehicle); final long id = testVehicle.id(); when(crudRepository.findByIdOptional(id)).thenReturn(Optional.of(vehicleEntity)); Vehicle removed = repository.removeVehicle(id); assertThat(removed).isEqualTo(testVehicle); verify(crudRepository).deleteById(id); } @Test void removing_nonexistent_vehicle_should_fail() { when(crudRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty()); // removing nonexistent vehicle should fail and its ID should appear in the exception message int uniqueNonexistentId = 7173; assertThatIllegalArgumentException() .isThrownBy(() -> repository.removeVehicle(uniqueNonexistentId)) .withMessageContaining(String.valueOf(uniqueNonexistentId)); } @Test void remove_all_vehicles() { repository.removeAll(); verify(crudRepository).deleteAll(); } @Test void get_all_vehicles() { VehicleEntity vehicleEntity = vehicleEntity(testVehicle); when(crudRepository.streamAll()).thenReturn(Stream.of(vehicleEntity)); assertThat(repository.vehicles()).containsExactly(testVehicle); } @Test void find_by_id() { VehicleEntity vehicleEntity = vehicleEntity(testVehicle); when(crudRepository.findByIdOptional(testVehicle.id())).thenReturn(Optional.of(vehicleEntity)); assertThat(repository.find(testVehicle.id())).contains(testVehicle); } @Test void update() { long vehicleId = 123; String name = "xy"; int capacity = 80; VehicleEntity vehicleEntity = new VehicleEntity(vehicleId, name, capacity - 10); when(crudRepository.findByIdOptional(vehicleId)).thenReturn(Optional.of(vehicleEntity)); Vehicle vehicle = repository.changeCapacity(vehicleId, capacity); verify(crudRepository).flush(); assertThat(vehicleEntity.getCapacity()).isEqualTo(capacity); assertThat(vehicle.id()).isEqualTo(vehicleId); assertThat(vehicle.name()).isEqualTo(name); assertThat(vehicle.capacity()).isEqualTo(capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/DistanceMapImplTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory; import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow; class DistanceMapImplTest { @Test void matrix_row_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new DistanceMapImpl(null)); } @Test void distance_map_should_return_value_from_distance_matrix_row() { PlanningLocation location2 = PlanningLocationFactory.testLocation(2); Distance distance = Distance.ofMillis(45000); DistanceMatrixRow matrixRow = locationId -> distance; DistanceMapImpl distanceMap = new DistanceMapImpl(matrixRow); assertThat(distanceMap.distanceTo(location2)).isEqualTo(distance.millis()); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/MockSolver.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; import org.mockito.Mockito; import org.optaplanner.core.api.solver.change.ProblemChange; import org.optaplanner.test.api.solver.change.MockProblemChangeDirector; public class MockSolver { private final Solution_ workingSolution; private final MockProblemChangeDirector changeDirector; public static MockSolver build(Solution_ solution) { MockProblemChangeDirector spy = Mockito.spy(new MockProblemChangeDirector()); return new MockSolver<>(solution, spy); } private MockSolver(Solution_ workingSolution, MockProblemChangeDirector changeDirector) { this.workingSolution = workingSolution; this.changeDirector = changeDirector; } // ************************************************************************ // Problem change API from Solver. // ************************************************************************ public void addProblemChange(ProblemChange problemChange) { problemChange.doChange(workingSolution, changeDirector); } // ************************************************************************ // Lookup API from MockProblemChangeDirector. // ************************************************************************ public MockProblemChangeDirector.LookUpMockBuilder whenLookingUp(Object forObject) { return changeDirector.whenLookingUp(forObject); } // ************************************************************************ // Simplified verification API. // ************************************************************************ public void verifyEntityAdded(Object entity) { verify(changeDirector).addEntity(same(entity), any()); } public void verifyEntityRemoved(Object entity) { verify(changeDirector).removeEntity(same(entity), any()); } public void verifyVariableChanged(Object entity, String variableName) { verify(changeDirector).changeVariable(same(entity), eq(variableName), any()); } public void verifyProblemFactAdded(Object fact) { verify(changeDirector).addProblemFact(same(fact), any()); } public void verifyProblemFactRemoved(Object fact) { verify(changeDirector).removeProblemFact(same(fact), any()); } public void verifyProblemPropertyChanged(Object entityOrFact) { verify(changeDirector).changeProblemProperty(same(entityOrFact), any()); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/RouteChangedEventPublisherTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.testLocation; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory.testVehicle; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit; import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.solutionFromVisits; import javax.enterprise.event.Event; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.optaweb.vehiclerouting.service.route.RouteChangedEvent; import org.optaweb.vehiclerouting.service.route.ShallowRoute; @ExtendWith(MockitoExtension.class) class RouteChangedEventPublisherTest { @Mock private Event publisher; @InjectMocks private RouteChangedEventPublisher routeChangedEventPublisher; @Test void should_covert_solution_to_event_and_publish_it() { routeChangedEventPublisher.publishSolution(SolutionFactory.emptySolution()); verify(publisher).fire(any(RouteChangedEvent.class)); } @Test void empty_solution_should_have_zero_routes_vehicles_etc() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this); assertThat(event.vehicleIds()).isEmpty(); assertThat(event.depotId()).isEmpty(); assertThat(event.visitIds()).isEmpty(); assertThat(event.routes()).isEmpty(); assertThat(event.distance()).isEqualTo(Distance.ZERO); } @Test void solution_with_vehicles_and_no_depot_should_have_zero_routes() { long vehicleId = 1; PlanningVehicle vehicle = testVehicle(vehicleId); VehicleRoutingSolution solution = solutionFromVisits(singletonList(vehicle), null, emptyList()); RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this); assertThat(event.vehicleIds()).containsExactly(vehicleId); assertThat(event.depotId()).isEmpty(); assertThat(event.visitIds()).isEmpty(); assertThat(event.routes()).isEmpty(); assertThat(event.distance()).isEqualTo(Distance.ZERO); } @Test void nonempty_solution_without_vehicles_should_have_zero_routes_but_contain_visits() { long depotId = 1; long visitId = 2; VehicleRoutingSolution solution = solutionFromVisits( emptyList(), new PlanningDepot(testLocation(depotId)), singletonList(testVisit(visitId))); RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this); assertThat(event.vehicleIds()).isEmpty(); assertThat(event.depotId()).contains(depotId); assertThat(event.visitIds()).containsExactly(visitId); assertThat(event.routes()).isEmpty(); assertThat(event.distance()).isEqualTo(Distance.ZERO); } @Test void initialized_solution_should_have_one_route_per_vehicle() { // arrange long vehicleId1 = 1001; long vehicleId2 = 2001; PlanningVehicle vehicle1 = testVehicle(vehicleId1); PlanningVehicle vehicle2 = testVehicle(vehicleId2); long depotId = 1; long visitId1 = 2; long visitId2 = 3; PlanningDepot depot = new PlanningDepot(testLocation(depotId)); PlanningVisit visit1 = testVisit(visitId1); PlanningVisit visit2 = testVisit(visitId2); VehicleRoutingSolution solution = solutionFromVisits( asList(vehicle1, vehicle2), depot, asList(visit1, visit2)); // Send both vehicles to both visits // V1 // \ // |---> visit1 ---> visit2 // / // V2 for (PlanningVehicle vehicle : solution.getVehicleList()) { vehicle.setNextVisit(visit1); visit1.setPreviousStandstill(vehicle); } visit1.setNextVisit(visit2); visit2.setPreviousStandstill(visit1); long softScore = -544564731; solution.setScore(HardSoftLongScore.ofSoft(softScore)); // act RouteChangedEvent event = RouteChangedEventPublisher.solutionToEvent(solution, this); // assert assertThat(event.routes()).hasSameSizeAs(solution.getVehicleList()); assertThat(event.routes().stream().mapToLong(value -> value.vehicleId)) .containsExactlyInAnyOrder(vehicleId1, vehicleId2); for (ShallowRoute route : event.routes()) { assertThat(route.depotId).isEqualTo(depot.getId()); // visits shouldn't include the depot assertThat(route.visitIds).containsExactly(visitId1, visitId2); } assertThat(event.vehicleIds()).containsExactlyInAnyOrder(vehicleId1, vehicleId2); assertThat(event.depotId()).contains(depotId); assertThat(event.visitIds()).containsExactlyInAnyOrder(visitId1, visitId2); assertThat(event.distance()).isEqualTo(Distance.ofMillis(-softScore)); } @Test void fail_fast_if_vehicles_next_visit_doesnt_exist() { PlanningVehicle vehicle = testVehicle(1); vehicle.setNextVisit(testVisit(2)); VehicleRoutingSolution solution = solutionFromVisits( singletonList(vehicle), new PlanningDepot(testLocation(1)), singletonList(testVisit(3))); assertThatIllegalArgumentException() .isThrownBy(() -> RouteChangedEventPublisher.solutionToEvent(solution, this)) .withMessageContaining("Visit"); } @Test void vehicle_without_a_depot_is_illegal_if_depot_exists() { PlanningDepot depot = new PlanningDepot(testLocation(1)); PlanningVehicle vehicle = testVehicle(1); VehicleRoutingSolution solution = solutionFromVisits(singletonList(vehicle), depot, emptyList()); vehicle.setDepot(null); assertThatIllegalArgumentException() .isThrownBy(() -> RouteChangedEventPublisher.solutionToEvent(solution, this)) .withMessageContaining("Vehicle"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/RouteOptimizerImplTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.optaweb.vehiclerouting.domain.VehicleFactory.createVehicle; import static org.optaweb.vehiclerouting.domain.VehicleFactory.testVehicle; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.fromDomain; import java.util.Arrays; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow; @ExtendWith(MockitoExtension.class) class RouteOptimizerImplTest { private final DistanceMatrixRow matrixRow = locationId -> Distance.ZERO; private final Location location1 = new Location(1, Coordinates.of(1.0, 0.1)); private final Location location2 = new Location(2, Coordinates.of(0.2, 2.2)); private final Location location3 = new Location(3, Coordinates.of(3.4, 5.6)); @Captor private ArgumentCaptor solutionArgumentCaptor; @Captor private ArgumentCaptor vehicleArgumentCaptor; @Mock private SolverManager solverManager; @Mock private RouteChangedEventPublisher routeChangedEventPublisher; @InjectMocks private RouteOptimizerImpl routeOptimizer; @Test void solution_with_depot_and_no_visits_should_be_published() { // arrange Long[] vehicleIds = { 2L, 3L, 5L, 7L, 11L }; Arrays.stream(vehicleIds).forEach(vehicleId -> routeOptimizer.addVehicle(testVehicle(vehicleId))); clearInvocations(routeChangedEventPublisher); // act routeOptimizer.addLocation(location1, matrixRow); // assert verifyNoInteractions(solverManager); VehicleRoutingSolution solution = verifyPublishingPreliminarySolution(); assertThat(solution.getVehicleList()) .extracting(PlanningVehicle::getId) .containsExactlyInAnyOrder(vehicleIds); assertThat(solution.getDepotList()).extracting(PlanningDepot::getId).containsExactly(location1.id()); assertThat(solution.getVisitList()).isEmpty(); } @Test void solution_with_vehicles_and_no_depot_should_be_published() { // arrange final long vehicleId = 7; final Vehicle vehicle = testVehicle(vehicleId); // act 1 routeOptimizer.addVehicle(vehicle); // assert 1 verifyNoInteractions(solverManager); VehicleRoutingSolution solutionWithOneVehicle = verifyPublishingPreliminarySolution(); assertThat(solutionWithOneVehicle.getVehicleList()) .extracting(PlanningVehicle::getId) .containsExactly(vehicleId); assertThat(solutionWithOneVehicle.getDepotList()).isEmpty(); assertThat(solutionWithOneVehicle.getVisitList()).isEmpty(); // act 2 clearInvocations(routeChangedEventPublisher); routeOptimizer.removeVehicle(vehicle); // assert 2 verifyNoInteractions(solverManager); VehicleRoutingSolution emptySolution = verifyPublishingPreliminarySolution(); assertThat(emptySolution.getVehicleList()).isEmpty(); assertThat(emptySolution.getDepotList()).isEmpty(); assertThat(emptySolution.getVisitList()).isEmpty(); } @Test void removing_wrong_vehicle_should_fail_fast() { // arrange final long vehicleId = 7; final Vehicle vehicle = testVehicle(vehicleId); final Vehicle nonExistentVehicle = testVehicle(vehicleId + 1); routeOptimizer.addVehicle(vehicle); // act & assert assertThatIllegalArgumentException() .isThrownBy(() -> routeOptimizer.removeVehicle(nonExistentVehicle)) .withMessageContaining("exist"); } @Test void removing_wrong_location_should_fail_fast() { // no locations assertThatIllegalArgumentException() .isThrownBy(() -> routeOptimizer.removeLocation(location1)) .withMessageContaining("no locations"); // only depot routeOptimizer.addLocation(location1, matrixRow); assertThatIllegalArgumentException() .isThrownBy(() -> routeOptimizer.removeLocation(location3)) .withMessageContaining("exist"); // depot and a visit routeOptimizer.addLocation(location2, matrixRow); assertThatIllegalArgumentException() .isThrownBy(() -> routeOptimizer.removeLocation(location3)) .withMessageContaining("exist"); } @Test void added_vehicle_should_be_moved_to_the_depot_even_if_solver_is_not_yet_solving() { // arrange // -- depot routeOptimizer.addLocation(location1, matrixRow); // -- vehicles routeOptimizer.addVehicle(testVehicle(7)); routeOptimizer.addVehicle(testVehicle(8)); // act // -- first visit routeOptimizer.addLocation(location2, matrixRow); // assert VehicleRoutingSolution solution = verifySolverStartedWithSolution(); assertThat(solution.getVehicleList()) .hasSize(2) .allMatch(vehicle -> vehicle.getDepot().getId() == location1.id()); assertThat(solution.getDepotList()).isNotEmpty(); assertThat(solution.getVisitList()).isNotEmpty(); } @Test void solver_should_start_when_vehicle_is_added_and_there_is_at_least_one_visit() { // arrange routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verifyNoInteractions(solverManager); // act routeOptimizer.addVehicle(testVehicle(9)); // assert VehicleRoutingSolution solution = verifySolverStartedWithSolution(); assertThat(solution.getVehicleList()).hasSize(1); assertThat(solution.getVisitList()).hasSize(1); } @Test void each_location_should_have_a_distance_map_after_it_is_added() { long millis = 8079; routeOptimizer.addLocation(location1, locationId -> Distance.ofMillis(millis)); VehicleRoutingSolution solution = verifyPublishingPreliminarySolution(); assertThat(solution.getDepotList()).hasSize(1); assertThat(solution.getDepotList().get(0).getLocation().distanceTo(fromDomain(location2))).isEqualTo(millis); } @Test void solver_should_start_when_two_locations_added_and_there_is_at_least_one_vehicle() { // add 1 vehicle, 2 locations routeOptimizer.addVehicle(testVehicle(1)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); // solving has started after adding a second location (=> depot + visit) VehicleRoutingSolution solution = verifySolverStartedWithSolution(); assertThat(solution.getDepotList()).hasSize(1); assertThat(solution.getDepotList().get(0).getLocation().getId()).isEqualTo(location1.id()); assertThat(solution.getVisitList()).hasSize(1); assertThat(solution.getVisitList().get(0).getLocation().getId()).isEqualTo(location2.id()); } @Test void solver_should_not_start_nor_stop_when_modifying_location_and_there_are_no_vehicles() { // add 2 locations routeOptimizer.addLocation(location1, matrixRow); clearInvocations(routeChangedEventPublisher); routeOptimizer.addLocation(location2, matrixRow); // solving did not start due to missing vehicles verify(solverManager, never()).startSolver(any()); // but preliminary solution is published VehicleRoutingSolution solution1 = verifyPublishingPreliminarySolution(); assertThat(solution1.getVehicleList()).isEmpty(); assertThat(solution1.getDepotList()).hasSize(1); assertThat(solution1.getVisitList()).hasSize(1); // add a third location and remove another one routeOptimizer.addLocation(location3, matrixRow); clearInvocations(routeChangedEventPublisher); routeOptimizer.removeLocation(location2); // no interactions with solver (start/stop/problem fact changes) because // it hasn't started (due to missing vehicles) verifyNoInteractions(solverManager); // but preliminary solution is published VehicleRoutingSolution solution2 = verifyPublishingPreliminarySolution(); assertThat(solution2.getVehicleList()).isEmpty(); assertThat(solution1.getDepotList()).hasSize(1); assertThat(solution1.getVisitList()).hasSize(1); } @Test void solver_should_stop_and_publish_when_last_vehicle_is_removed() { Vehicle vehicle = testVehicle(23); routeOptimizer.addVehicle(vehicle); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); clearInvocations(routeChangedEventPublisher); routeOptimizer.removeVehicle(vehicle); verify(solverManager).stopSolver(); VehicleRoutingSolution solution = verifyPublishingPreliminarySolution(); assertThat(solution.getVehicleList()).isEmpty(); } @Test void solver_should_stop_when_locations_reduced_to_one() { // add 1 vehicle, 2 locations routeOptimizer.addVehicle(testVehicle(0)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); clearInvocations(routeChangedEventPublisher); // remove 1 location from running solver routeOptimizer.removeLocation(location2); verify(solverManager).stopSolver(); VehicleRoutingSolution solution = verifyPublishingPreliminarySolution(); assertThat(solution.getVisitList()).isEmpty(); assertThat(solution.getDepotList()).hasSize(1); assertThat(solution.getVehicleList()).hasSize(1); } @Test void removing_depot_impossible_when_there_are_other_locations() { routeOptimizer.addVehicle(testVehicle(0)); // add 2 locations routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); assertThatIllegalStateException() .isThrownBy(() -> routeOptimizer.removeLocation(location1)) .withMessageContaining("depot"); } @Test void when_depot_is_added_all_vehicles_should_be_moved_to_it() { // given 2 vehicles long vehicleId1 = 8; long vehicleId2 = 113; routeOptimizer.addVehicle(testVehicle(vehicleId1)); routeOptimizer.addVehicle(testVehicle(vehicleId2)); clearInvocations(routeChangedEventPublisher); // when a depot is added routeOptimizer.addLocation(location1, matrixRow); // then all vehicles must be in the depot VehicleRoutingSolution solution1 = verifyPublishingPreliminarySolution(); assertThat(solution1.getVehicleList()) .extracting(PlanningVehicle::getId) .containsExactlyInAnyOrder(vehicleId1, vehicleId2); assertThat(solution1.getVehicleList()).allMatch(vehicle -> vehicle.getDepot().getId() == location1.id()); assertThat(solution1.getDepotList()).extracting(PlanningDepot::getId).containsExactly(location1.id()); // if we remove the depot clearInvocations(routeChangedEventPublisher); routeOptimizer.removeLocation(location1); // then published solution's depot list is empty VehicleRoutingSolution solution2 = verifyPublishingPreliminarySolution(); assertThat(solution2.getVehicleList()) .extracting(PlanningVehicle::getId) .containsExactlyInAnyOrder(vehicleId1, vehicleId2); assertThat(solution2.getDepotList()).isEmpty(); // and it's possible to add a new depot routeOptimizer.addLocation(location2, matrixRow); } @Test void adding_location_to_running_solver_must_happen_through_problem_fact_change() { // arrange routeOptimizer.addVehicle(testVehicle(55)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); // act routeOptimizer.addLocation(location3, matrixRow); // assert verify(solverManager).addVisit(any(PlanningVisit.class)); } @Test void removing_location_from_solver_with_more_than_two_locations_must_happen_through_problem_fact_change() { // arrange: set up a situation where solver is running with 1 depot and 2 visits long vehicleId = 0; routeOptimizer.addVehicle(testVehicle(vehicleId)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); // add second visit to avoid stopping solver manager after removing a visit below routeOptimizer.addLocation(location3, matrixRow); verify(solverManager).addVisit(any(PlanningVisit.class)); // act routeOptimizer.removeLocation(location2); // assert ArgumentCaptor visitArgumentCaptor = ArgumentCaptor.forClass(PlanningVisit.class); verify(solverManager).removeVisit(visitArgumentCaptor.capture()); assertThat(visitArgumentCaptor.getValue().getId()).isEqualTo(location2.id()); // solver still running verify(solverManager, never()).stopSolver(); } @Test void adding_vehicle_to_running_solver_must_happen_through_problem_fact_change() { // arrange routeOptimizer.addVehicle(testVehicle(1)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); // act routeOptimizer.addVehicle(testVehicle(22)); // assert verify(solverManager).addVehicle(vehicleArgumentCaptor.capture()); PlanningVehicle vehicle = vehicleArgumentCaptor.getValue(); assertThat(vehicle.getDepot().getId()).isEqualTo(location1.id()); } @Test void removing_vehicle_from_running_solver_with_more_than_one_vehicle_must_happen_through_problem_fact_change() { // arrange: set up a situation where solver is running with 2 vehicles final long vehicleId1 = 10; final long vehicleId2 = 20; routeOptimizer.addVehicle(testVehicle(vehicleId1)); routeOptimizer.addVehicle(testVehicle(vehicleId2)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); // act routeOptimizer.removeVehicle(testVehicle(vehicleId1)); // assert verify(solverManager).removeVehicle(any(PlanningVehicle.class)); // solver still running verify(solverManager, never()).stopSolver(); } @Test void changing_vehicle_capacity_should_take_effect_when_solver_is_started_or_be_published() { // 1 depot, 1 vehicle final long vehicleId = 1; final int oldCapacity = 7; final int newCapacity = 12; Vehicle vehicle = createVehicle(vehicleId, "", oldCapacity); routeOptimizer.addVehicle(vehicle); routeOptimizer.addLocation(location1, matrixRow); clearInvocations(routeChangedEventPublisher); // change capacity when solver is not running routeOptimizer.changeCapacity(createVehicle(vehicleId, "", newCapacity)); verifyNoInteractions(solverManager); VehicleRoutingSolution preliminarySolution = verifyPublishingPreliminarySolution(); assertThat(preliminarySolution.getVehicleList().get(0).getCapacity()).isEqualTo(newCapacity); // start solver routeOptimizer.addLocation(location2, matrixRow); VehicleRoutingSolution solution = verifySolverStartedWithSolution(); assertThat(solution.getVehicleList().get(0).getCapacity()).isEqualTo(newCapacity); } @Test void changing_vehicle_capacity_must_happen_through_problem_fact_change_when_solver_is_running() { // 1 vehicle, 1 depot, 1 visit final int capacity = 14816; final long vehicleId = 10; routeOptimizer.addVehicle(testVehicle(vehicleId)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); routeOptimizer.changeCapacity(createVehicle(vehicleId, "", capacity)); verify(solverManager).changeCapacity(any(PlanningVehicle.class)); } @Test void changing_vehicle_capacity_must_fail_fast_if_the_vehicle_does_not_exist() { // 1 vehicle, 1 depot, 1 visit final long vehicleId = 10; routeOptimizer.addVehicle(testVehicle(vehicleId)); assertThatIllegalArgumentException() .isThrownBy(() -> routeOptimizer.changeCapacity(testVehicle(vehicleId + 1))) .withMessageContaining("exist"); } @Test void remove_all_locations_should_stop_solver_and_publish_preliminary_solution() { // set up a situation where solver is running with 1 depot and 2 visits long vehicleId = 10; routeOptimizer.addVehicle(testVehicle(vehicleId)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); routeOptimizer.addLocation(location3, matrixRow); clearInvocations(routeChangedEventPublisher); routeOptimizer.removeAllLocations(); verify(solverManager).stopSolver(); VehicleRoutingSolution solution = verifyPublishingPreliminarySolution(); assertThat(solution.getVehicleList()).hasSize(1); assertThat(solution.getDepotList()).isEmpty(); assertThat(solution.getVisitList()).isEmpty(); } @Test void remove_all_vehicles_should_stop_solver_and_publish_preliminary_solution() { long vehicleId = 10; routeOptimizer.addVehicle(testVehicle(vehicleId)); routeOptimizer.addLocation(location1, matrixRow); routeOptimizer.addLocation(location2, matrixRow); verify(solverManager).startSolver(any(VehicleRoutingSolution.class)); routeOptimizer.addLocation(location3, matrixRow); clearInvocations(routeChangedEventPublisher); routeOptimizer.removeAllVehicles(); verify(solverManager).stopSolver(); VehicleRoutingSolution solution = verifyPublishingPreliminarySolution(); assertThat(solution.getVehicleList()).isEmpty(); assertThat(solution.getDepotList()).hasSize(1); assertThat(solution.getVisitList()).hasSize(2); } @Test void removing_all_locations_should_not_fail_when_solver_is_not_solving() { assertThatCode(() -> routeOptimizer.removeAllLocations()).doesNotThrowAnyException(); } @Test void removing_all_vehicles_should_not_fail_when_solver_is_not_solving() { assertThatCode(() -> routeOptimizer.removeAllVehicles()).doesNotThrowAnyException(); } private VehicleRoutingSolution verifyPublishingPreliminarySolution() { verify(routeChangedEventPublisher).publishSolution(solutionArgumentCaptor.capture()); return solutionArgumentCaptor.getValue(); } private VehicleRoutingSolution verifySolverStartedWithSolution() { verify(solverManager).startSolver(solutionArgumentCaptor.capture()); return solutionArgumentCaptor.getValue(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverExceptionTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import javax.enterprise.event.Event; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaplanner.core.api.solver.Solver; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.optaweb.vehiclerouting.service.error.ErrorEvent; import com.google.common.util.concurrent.ListenableFutureTask; import com.google.common.util.concurrent.ListeningExecutorService; @ExtendWith(MockitoExtension.class) class SolverExceptionTest { @Mock private Solver solver; @Mock private ListeningExecutorService executor; @Mock private Event eventPublisher; @Captor ArgumentCaptor errorEventArgumentCaptor; @InjectMocks private SolverManager solverManager; @Test void should_publish_error_if_solver_stops_solving_without_being_terminated() { // arrange // Prepare a future that will be returned by mock executor ListenableFutureTask task = ListenableFutureTask.create(SolutionFactory::emptySolution); when(executor.submit(any(SolverManager.SolvingTask.class))).thenReturn(task); // Run it synchronously (otherwise the test would be unreliable!) task.run(); // act solverManager.startSolver(SolutionFactory.emptySolution()); // assert verify(eventPublisher).fire(errorEventArgumentCaptor.capture()); assertThat(errorEventArgumentCaptor.getValue().message).contains("This is a bug."); } @Test void should_not_publish_error_if_solver_is_terminated_early() { // arrange // Prepare a future that will be returned by mock executor ListenableFutureTask task = ListenableFutureTask.create(SolutionFactory::emptySolution); when(executor.submit(any(SolverManager.SolvingTask.class))).thenReturn(task); // Pretend the solver has been terminated by stopSolver()... when(solver.isTerminateEarly()).thenReturn(true); // act solverManager.startSolver(SolutionFactory.emptySolution()); task.run(); // ...so that when this invokes the success callback, it won't publish an error // assert verifyNoInteractions(eventPublisher); } @Test void should_propagate_any_exception_from_solver() { // arrange // Prepare a future that will be returned by mock executor String exceptionMessage = "msg 123"; ListenableFutureTask task = ListenableFutureTask.create(() -> { throw new TestException(exceptionMessage); }); when(executor.submit(any(SolverManager.SolvingTask.class))).thenReturn(task); // act (1) // Run it synchronously (otherwise the test would be unreliable!) task.run(); solverManager.startSolver(SolutionFactory.emptySolution()); // assert (1) verify(eventPublisher).fire(errorEventArgumentCaptor.capture()); assertThat(errorEventArgumentCaptor.getValue().message) .contains(TestException.class.getName()) .contains(exceptionMessage); PlanningVisit planningVisit = PlanningVisitFactory.testVisit(1); PlanningVehicle planningVehicle = PlanningVehicleFactory.testVehicle(1); // act & assert (2) assertTestExceptionThrownDuringOperation(() -> solverManager.addVisit(planningVisit)); assertTestExceptionThrownDuringOperation(() -> solverManager.removeVisit(planningVisit)); assertTestExceptionThrownDuringOperation(() -> solverManager.addVehicle(planningVehicle)); assertTestExceptionThrownDuringOperation(() -> solverManager.removeVehicle(planningVehicle)); assertTestExceptionThrownWhenStoppingSolver(solverManager); } private static void assertTestExceptionThrownDuringOperation(ThrowingCallable runnable) { assertTestExceptionThrownDuring(runnable, "died"); } private static void assertTestExceptionThrownWhenStoppingSolver(SolverManager routeOptimizer) { assertTestExceptionThrownDuring(routeOptimizer::stopSolver, "stop"); } private static void assertTestExceptionThrownDuring(ThrowingCallable runnable, String message) { assertThatExceptionOfType(RuntimeException.class) .isThrownBy(runnable) .withMessageContaining(message) .withCauseInstanceOf(TestException.class); } private static class TestException extends RuntimeException { TestException(String message) { super(message); } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverIntegrationTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.testLocation; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.fromLocation; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit; import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.emptySolution; import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.solutionFromVisits; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent; import org.optaplanner.core.api.solver.event.SolverEventListener; import org.optaplanner.core.config.solver.SolverConfig; import org.optaweb.vehiclerouting.plugin.planner.change.AddVisit; import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.slf4j.Logger; import org.slf4j.LoggerFactory; class SolverIntegrationTest { private static final Logger logger = LoggerFactory.getLogger(SolverIntegrationTest.class); // Set a benevolent timeout to avoid issues in the CI environment. private static final int PFC_PROPAGATION_TIMEOUT_MILLIS = 10_000; private SolverConfig solverConfig; private ExecutorService executor; private ProblemChangeProcessingMonitor monitor; private Future futureSolution; @BeforeEach void setUp() { solverConfig = SolverConfig.createFromXmlResource(Constants.SOLVER_CONFIG); solverConfig.setDaemon(true); executor = Executors.newSingleThreadExecutor(); monitor = new ProblemChangeProcessingMonitor(); } @AfterEach void tearDown() throws InterruptedException { executor.shutdown(); executor.awaitTermination(1, TimeUnit.SECONDS); } @Disabled("Solver fails fast on empty value ranges") // TODO file an OptaPlanner ticket for empty value ranges @Test void solver_in_daemon_mode_should_not_fail_on_empty_solution() { Solver solver = SolverFactory. create(solverConfig).buildSolver(); assertThat(solver.solve(emptySolution())).isNotNull(); } // TODO remove vehicle, change capacity, change demand... @Test void removing_visits_should_not_fail() { long distance = 1; PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1); VehicleRoutingSolution solution = solutionFromVisits( singletonList(vehicle), new PlanningDepot(testLocation(1, location -> distance)), singletonList(fromLocation(testLocation(2, location -> distance)))); Solver solver = SolverFactory. create(solverConfig).buildSolver(); solver.addEventListener(monitor); startSolver(solver, solution); for (int id = 3; id < 6; id++) { logger.info("Add visit ({})", id); monitor.beforeProblemChange(); solver.addProblemChange(new AddVisit(fromLocation(testLocation(id, location -> distance)))); assertThat(monitor.awaitAllProblemChanges(PFC_PROPAGATION_TIMEOUT_MILLIS)).isTrue(); } List visitIds = Arrays.asList(5, 2, 3); for (int id : visitIds) { logger.info("Remove visit ({})", id); assertThat(solver.isEveryProblemChangeProcessed()).isTrue(); monitor.beforeProblemChange(); solver.addProblemChange(new RemoveVisit(testVisit(id))); assertThat(solver.isEveryProblemChangeProcessed()).isFalse(); // probably not 100% safe // Notice that it's not possible to check individual problem fact changes completion. // When we receive a BestSolutionChangedEvent with unprocessed PFCs, // we don't know how many of them there are. if (!monitor.awaitAllProblemChanges(PFC_PROPAGATION_TIMEOUT_MILLIS)) { assertThat(terminateSolver(solver)).isNotNull(); fail("Problem fact change hasn't been completed"); } } assertThat(terminateSolver(solver)).isNotNull(); } private void startSolver(Solver solver, VehicleRoutingSolution solution) { futureSolution = executor.submit(() -> solver.solve(solution)); } private VehicleRoutingSolution terminateSolver(Solver solver) { solver.terminateEarly(); try { return futureSolution.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); fail("Interrupted", e); } catch (ExecutionException e) { fail("Solving failed", e); } throw new AssertionError(); } static class ProblemChangeProcessingMonitor implements SolverEventListener { private static final Logger logger = LoggerFactory.getLogger(ProblemChangeProcessingMonitor.class); private final Semaphore problemChanges = new Semaphore(0); void beforeProblemChange() { int permitsDrained = problemChanges.drainPermits(); logger.debug("Before PFC (permits drained: {})", permitsDrained); } boolean awaitAllProblemChanges(int milliseconds) { // Available permits may rarely be > 0 if the PFC completes before we start waiting, // or if the solution has improved since we called beforePFC() => the test is not completely reliable. logger.debug("WAIT (completed PFCs: {})", problemChanges.availablePermits()); try { if (problemChanges.tryAcquire(milliseconds, TimeUnit.MILLISECONDS)) { logger.info("Problem Fact Change DONE"); return true; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); fail("Interrupted", e); } return false; } @Override public void bestSolutionChanged(BestSolutionChangedEvent event) { // This happens on solver thread if (!event.isEveryProblemChangeProcessed()) { logger.debug("UNPROCESSED"); } else if (!event.getNewBestScore().isSolutionInitialized()) { logger.debug("UNINITIALIZED ({})", event.getNewBestScore()); } else { logger.debug("New best solution (COMPLETE)"); problemChanges.release(); } } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverManagerIntegrationTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThatCode; import static org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory.solutionFromVisits; import java.util.concurrent.Semaphore; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.optaweb.vehiclerouting.plugin.planner.domain.DistanceMap; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.optaweb.vehiclerouting.service.route.RouteChangedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; @QuarkusTest @TestProfile(SolverTestProfile.class) class SolverManagerIntegrationTest { @Inject SolverManager solverManager; @Inject RouteChangedEventSemaphore routeChangedEventSemaphore; private static DistanceMap mockDistanceMap() { return location -> 60; } @Test @Timeout(value = 60) void solver_should_be_in_daemon_mode() throws InterruptedException { PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1); PlanningLocation depot = PlanningLocationFactory.testLocation(1, mockDistanceMap()); PlanningLocation visit = PlanningLocationFactory.testLocation(2, mockDistanceMap()); VehicleRoutingSolution solution = solutionFromVisits( singletonList(vehicle), new PlanningDepot(depot), singletonList(PlanningVisitFactory.fromLocation(visit))); solverManager.startSolver(solution); // Waits until the solution is initialized. There is only 1 possible step => no more than 1 RouteChangedEvent. routeChangedEventSemaphore.waitForRouteUpdate(); // The best solution has been updated. We know the score must be -1hard/-120soft because that's // the only possible solution. The termination property is set exactly to this score => we know // the solver is now terminated. // If the solver is in daemon mode, it doesn't return from solve() although solving has ended. // Instead, it's actively waiting for a PFC and will restart once it arrives from the outside (the test thread). // If it's not in daemon mode, it returns from solve() method once the termination condition is met // and the following PFC attempt fails. assertThatCode(() -> solverManager.changeCapacity(vehicle)).doesNotThrowAnyException(); } @ApplicationScoped static class RouteChangedEventSemaphore { private static final Logger logger = LoggerFactory.getLogger(RouteChangedEventSemaphore.class); private final Semaphore semaphore = new Semaphore(0); public void onApplicationEvent(@Observes RouteChangedEvent event) { logger.info("DISTANCE: {}", event.distance()); semaphore.release(); } void waitForRouteUpdate() throws InterruptedException { semaphore.acquire(); int remainingPermits = semaphore.availablePermits(); if (remainingPermits > 0) { throw new IllegalStateException( "Only 1 RouteChangedEvent was expected but there were at least " + (remainingPermits + 1)); } } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverManagerTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.AdditionalAnswers.answer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer1; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent; import org.optaweb.vehiclerouting.plugin.planner.change.AddVehicle; import org.optaweb.vehiclerouting.plugin.planner.change.AddVisit; import org.optaweb.vehiclerouting.plugin.planner.change.ChangeVehicleCapacity; import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVehicle; import org.optaweb.vehiclerouting.plugin.planner.change.RemoveVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @ExtendWith(MockitoExtension.class) class SolverManagerTest { private final VehicleRoutingSolution solution = SolutionFactory.emptySolution(); private final PlanningVehicle testVehicle = PlanningVehicleFactory.testVehicle(1); private final PlanningVisit testVisit = PlanningVisitFactory.testVisit(1); @Captor private ArgumentCaptor solutionArgumentCaptor; @Mock private BestSolutionChangedEvent bestSolutionChangedEvent; @Mock private ListenableFuture solverFuture; @Mock private Solver solver; @Mock private ListeningExecutorService executor; @Mock private RouteChangedEventPublisher routeChangedEventPublisher; @InjectMocks private SolverManager solverManager; private void returnSolverFutureWhenSolverIsStarted() { // always run the runnable submitted to executor (that's what every executor does) // we can then verify that solver.solve() has been called when(executor.submit(any(SolverManager.SolvingTask.class))).thenAnswer( answer((Answer1, SolverManager.SolvingTask>) callable -> { callable.call(); return solverFuture; })); } @Test void should_listen_for_best_solution_events() { verify(solver).addEventListener(solverManager); } @Test void ignore_new_best_solutions_when_unprocessed_fact_changes() { // arrange when(bestSolutionChangedEvent.isEveryProblemChangeProcessed()).thenReturn(false); // act solverManager.bestSolutionChanged(bestSolutionChangedEvent); // assert verify(bestSolutionChangedEvent, never()).getNewBestSolution(); verify(routeChangedEventPublisher, never()).publishSolution(any()); } @Test void publish_new_best_solution_if_all_fact_changes_processed() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); when(bestSolutionChangedEvent.isEveryProblemChangeProcessed()).thenReturn(true); when(bestSolutionChangedEvent.getNewBestSolution()).thenReturn(solution); solverManager.bestSolutionChanged(bestSolutionChangedEvent); verify(routeChangedEventPublisher).publishSolution(solutionArgumentCaptor.capture()); VehicleRoutingSolution event = solutionArgumentCaptor.getValue(); assertThat(event).isSameAs(solution); } @Test void startSolver_should_start_solver() { returnSolverFutureWhenSolverIsStarted(); solverManager.startSolver(solution); verify(solver).solve(solution); // cannot start solver that is already solving assertThatIllegalStateException() .isThrownBy(() -> solverManager.startSolver(solution)); } @Test void stopSolver_should_terminate_solver() { returnSolverFutureWhenSolverIsStarted(); solverManager.startSolver(solution); solverManager.stopSolver(); verify(solver).terminateEarly(); // another stopSolver() does nothing solverManager.stopSolver(); // This verifies there were no more invocations of terminateEarly() without clearing all invocations. // Not using Mockito.clearInvocations() only because it doesn't like generic arguments. verify(solver).terminateEarly(); } @Test void reset_interrupted_flag() throws ExecutionException, InterruptedException { returnSolverFutureWhenSolverIsStarted(); // start solver solverManager.startSolver(solution); when(solverFuture.isDone()).thenReturn(true); when(solverFuture.get()).thenThrow(InterruptedException.class); PlanningVisit visit = testVisit(0); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> solverManager.removeVisit(visit)); assertThat(Thread.interrupted()).isTrue(); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> solverManager.stopSolver()); assertThat(Thread.interrupted()).isTrue(); } @Test void change_operations_should_fail_if_solver_has_not_started_yet() { assertThatIllegalStateException() .isThrownBy(() -> solverManager.addVehicle(testVehicle)) .withMessageContaining("started"); assertThatIllegalStateException() .isThrownBy(() -> solverManager.removeVehicle(testVehicle)) .withMessageContaining("started"); assertThatIllegalStateException() .isThrownBy(() -> solverManager.changeCapacity(testVehicle)) .withMessageContaining("started"); assertThatIllegalStateException() .isThrownBy(() -> solverManager.addVisit(testVisit)) .withMessageContaining("started"); assertThatIllegalStateException() .isThrownBy(() -> solverManager.removeVisit(testVisit)) .withMessageContaining("started"); } @Test void change_operations_should_fail_is_solver_has_died() throws ExecutionException, InterruptedException { returnSolverFutureWhenSolverIsStarted(); solverManager.startSolver(solution); when(solverFuture.isDone()).thenReturn(true); when(solverFuture.get()).thenThrow(ExecutionException.class); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> solverManager.addVehicle(testVehicle)) .withMessageContaining("died"); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> solverManager.removeVehicle(testVehicle)) .withMessageContaining("died"); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> solverManager.changeCapacity(testVehicle)) .withMessageContaining("died"); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> solverManager.addVisit(testVisit)) .withMessageContaining("died"); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> solverManager.removeVisit(testVisit)) .withMessageContaining("died"); } @Test void change_operations_should_submit_problem_fact_changes_to_solver() { returnSolverFutureWhenSolverIsStarted(); solverManager.startSolver(solution); when(solverFuture.isDone()).thenReturn(false); solverManager.addVehicle(testVehicle); verify(solver).addProblemChange(any(AddVehicle.class)); solverManager.removeVehicle(testVehicle); verify(solver).addProblemChange(any(RemoveVehicle.class)); solverManager.changeCapacity(testVehicle); verify(solver).addProblemChange(any(ChangeVehicleCapacity.class)); solverManager.addVisit(testVisit); verify(solver).addProblemChange(any(AddVisit.class)); solverManager.removeVisit(testVisit); verify(solver).addProblemChange(any(RemoveVisit.class)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/SolverTestProfile.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import java.util.HashMap; import java.util.Map; import io.quarkus.test.junit.QuarkusTestProfile; public class SolverTestProfile implements QuarkusTestProfile { @Override public Map getConfigOverrides() { HashMap config = new HashMap<>(); config.put("quarkus.optaplanner.solver.termination.best-score-limit", "-1hard/-120soft"); return config; } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/VehicleRoutingConstraintProviderTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.testLocation; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.fromLocation; import org.junit.jupiter.api.Test; import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; import org.optaplanner.test.api.score.stream.ConstraintVerifier; import org.optaweb.vehiclerouting.plugin.planner.domain.DistanceMap; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.Standstill; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; class VehicleRoutingConstraintProviderTest { private final ConstraintVerifier constraintVerifier = ConstraintVerifier.build( new VehicleRoutingConstraintProvider(), VehicleRoutingSolution.class, Standstill.class, PlanningVisit.class); private static DistanceMap distanceToAll(long distance) { return location -> distance; } private static void route(PlanningVehicle vehicle, PlanningVisit... visits) { Standstill previousStandstill = vehicle; for (PlanningVisit visit : visits) { visit.setVehicle(vehicle); visit.setPreviousStandstill(previousStandstill); previousStandstill.setNextVisit(visit); previousStandstill = visit; } } @Test void vehicle_capacity_penalized_1vehicle_1visit() { int demand = 100; int capacity = 5; PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1, capacity); vehicle.setDepot(new PlanningDepot(testLocation(1, distanceToAll(0)))); PlanningVisit visit = fromLocation(testLocation(2, distanceToAll(0)), demand); route(vehicle, visit); constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity) .given(visit) .penalizesBy(demand - capacity); } @Test void vehicle_capacity_penalized_1vehicle_3visits() { int demand1 = 4; int demand2 = 3; int demand3 = 9; int capacity = 5; PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1, capacity); vehicle.setDepot(new PlanningDepot(testLocation(0, distanceToAll(0)))); PlanningVisit visit1 = fromLocation(testLocation(1, distanceToAll(0)), demand1); PlanningVisit visit2 = fromLocation(testLocation(2, distanceToAll(0)), demand2); PlanningVisit visit3 = fromLocation(testLocation(3, distanceToAll(0)), demand3); route(vehicle, visit1, visit2, visit3); constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity) .given(visit1, visit2, visit3) .penalizesBy(demand1 + demand2 + demand3 - capacity); } @Test void capacity_not_penalized_when_greater_or_equal_to_demand() { int demand1 = 4; int demand2 = 3; int demand3 = 9; int totalDemand = demand1 + demand2 + demand3; PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1, totalDemand); vehicle.setDepot(new PlanningDepot(testLocation(0, distanceToAll(0)))); PlanningVisit visit1 = fromLocation(testLocation(1, distanceToAll(0)), demand1); PlanningVisit visit2 = fromLocation(testLocation(2, distanceToAll(0)), demand2); PlanningVisit visit3 = fromLocation(testLocation(3, distanceToAll(0)), demand3); route(vehicle, visit1, visit2, visit3); constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity) .given(visit1, visit2, visit3) .penalizesBy(0); // test values near the constraint boundary vehicle.setCapacity(totalDemand + 1); constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity) .given(visit1, visit2, visit3) .penalizesBy(0); } @Test void vehicles_capacity_constraint_should_work_for_multiple_vehicles() { int demand1a = 11; int demand1b = 12; int demand1c = 14; int demand2a = 3000; int demand2b = 2500; int demand2c = 8000; int capacity1 = demand1a + demand1b + demand1c; int capacity2 = demand2a + demand2b + demand2c; PlanningDepot depot = new PlanningDepot(testLocation(0, distanceToAll(0))); PlanningVehicle vehicle1 = PlanningVehicleFactory.testVehicle(1, capacity1); vehicle1.setDepot(depot); PlanningVehicle vehicle2 = PlanningVehicleFactory.testVehicle(2, capacity2); vehicle2.setDepot(depot); PlanningVisit visit1 = fromLocation(testLocation(1, distanceToAll(0)), demand1a); PlanningVisit visit2 = fromLocation(testLocation(2, distanceToAll(0)), demand1b); PlanningVisit visit3 = fromLocation(testLocation(3, distanceToAll(0)), demand1c); PlanningVisit visit4 = fromLocation(testLocation(4, distanceToAll(0)), demand2a); PlanningVisit visit5 = fromLocation(testLocation(5, distanceToAll(0)), demand2b); PlanningVisit visit6 = fromLocation(testLocation(6, distanceToAll(0)), demand2c); route(vehicle1, visit1, visit2, visit3); route(vehicle2, visit4, visit5, visit6); constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity) .given(visit1, visit2, visit3, visit4, visit5, visit6) .penalizesBy(0); vehicle1.setCapacity(capacity1 - 3); vehicle2.setCapacity(capacity2 - 7); constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity) .given(visit1, visit2, visit3, visit4, visit5, visit6) .penalizesBy(10); } @Test void distance_2vehicles() { int fromDepot1 = 1000; int fromDepot2 = 2000; PlanningDepot depot1 = new PlanningDepot(testLocation(0, distanceToAll(fromDepot1))); PlanningDepot depot2 = new PlanningDepot(testLocation(0, distanceToAll(fromDepot2))); PlanningVehicle vehicle1 = PlanningVehicleFactory.testVehicle(1, Integer.MAX_VALUE); vehicle1.setDepot(depot1); PlanningVehicle vehicle2 = PlanningVehicleFactory.testVehicle(1, Integer.MAX_VALUE); vehicle2.setDepot(depot2); int fromA = 17; int fromB = 11; int fromC = 37; int fromD = 123; int fromE = 77; int fromF = 99; PlanningVisit visitA = fromLocation(testLocation(1, distanceToAll(fromA))); PlanningVisit visitB = fromLocation(testLocation(2, distanceToAll(fromB))); PlanningVisit visitC = fromLocation(testLocation(3, distanceToAll(fromC))); PlanningVisit visitD = fromLocation(testLocation(4, distanceToAll(fromD))); PlanningVisit visitE = fromLocation(testLocation(5, distanceToAll(fromE))); PlanningVisit visitF = fromLocation(testLocation(6, distanceToAll(fromF))); route(vehicle1, visitA, visitB, visitC); route(vehicle2, visitD, visitE, visitF); // vehicle 1: depot→last constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromPreviousStandstill) .given(vehicle1, visitA, visitB, visitC) .penalizesBy(fromDepot1 + fromA + fromB); // vehicle 1: last→depot constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromLastVisitToDepot) .given(vehicle1, visitA, visitB, visitC) .penalizesBy(fromC); // vehicle 2: depot→last constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromPreviousStandstill) .given(vehicle2, visitD, visitE, visitF) .penalizesBy(fromDepot2 + fromD + fromE); // vehicle 2: last→depot constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromLastVisitToDepot) .given(vehicle2, visitD, visitE, visitF) .penalizesBy(fromF); // vehicles 1+2: depot→last constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromPreviousStandstill) .given(vehicle1, vehicle2, visitA, visitB, visitC, visitD, visitE, visitF) .penalizesBy(fromDepot1 + fromDepot2 + fromA + fromB + fromD + fromE); // vehicles 1+2: last→depot constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::distanceFromLastVisitToDepot) .given(vehicle1, vehicle2, visitA, visitB, visitC, visitD, visitE, visitF) .penalizesBy(fromC + fromF); // score constraintVerifier.verifyThat() .given(vehicle1, vehicle2, visitA, visitB, visitC, visitD, visitE, visitF) .scores(HardSoftLongScore.ofSoft( -(fromDepot1 + fromDepot2 + fromA + fromB + fromC + fromD + fromE + fromF))); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/change/AddVehicleTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.plugin.planner.MockSolver; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; class AddVehicleTest { @Test void add_vehicle_should_add_vehicle() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); MockSolver mockSolver = MockSolver.build(solution); PlanningVehicle vehicle = PlanningVehicleFactory.testVehicle(1); mockSolver.addProblemChange(new AddVehicle(vehicle)); assertThat(solution.getVehicleList()).containsExactly(vehicle); mockSolver.verifyProblemFactAdded(vehicle); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/change/AddVisitTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.plugin.planner.MockSolver; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; class AddVisitTest { @Test void add_visit_should_add_location_and_create_visit() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); MockSolver mockSolver = MockSolver.build(solution); PlanningVisit visit = PlanningVisitFactory.testVisit(1); mockSolver.addProblemChange(new AddVisit(visit)); mockSolver.verifyEntityAdded(visit); assertThat(solution.getVisitList()).containsExactly(visit); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/change/ChangeVehicleCapacityTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.plugin.planner.MockSolver; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; class ChangeVehicleCapacityTest { @Test void change_vehicle_capacity() { int oldCapacity = 100; int newCapacity = 50; MockSolver mockSolver = MockSolver.build(SolutionFactory.emptySolution()); PlanningVehicle workingVehicle = PlanningVehicleFactory.testVehicle(1, oldCapacity); PlanningVehicle changeVehicle = PlanningVehicleFactory.testVehicle(2, newCapacity); mockSolver.whenLookingUp(changeVehicle).thenReturn(workingVehicle); // do change mockSolver.addProblemChange(new ChangeVehicleCapacity(changeVehicle)); assertThat(workingVehicle.getCapacity()).isEqualTo(newCapacity); mockSolver.verifyProblemPropertyChanged(changeVehicle); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/change/RemoveVehicleTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Test; import org.optaplanner.test.api.solver.change.MockProblemChangeDirector; import org.optaweb.vehiclerouting.plugin.planner.MockSolver; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicle; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; class RemoveVehicleTest { @Test void remove_vehicle() { PlanningVehicle removedVehicle = PlanningVehicleFactory.testVehicle(1); PlanningVehicle otherVehicle = PlanningVehicleFactory.testVehicle(2); PlanningDepot depot = new PlanningDepot(PlanningLocationFactory.testLocation(1)); PlanningVisit firstVisit = testVisit(1); PlanningVisit lastVisit = testVisit(2); VehicleRoutingSolution solution = SolutionFactory.solutionFromVisits( Arrays.asList(removedVehicle, otherVehicle), depot, Arrays.asList(firstVisit, lastVisit)); MockSolver mockSolver = MockSolver.build(solution); // V -> first -> last removedVehicle.setNextVisit(firstVisit); firstVisit.setPreviousStandstill(removedVehicle); firstVisit.setVehicle(removedVehicle); firstVisit.setNextVisit(lastVisit); lastVisit.setPreviousStandstill(firstVisit); lastVisit.setVehicle(removedVehicle); // do change mockSolver.addProblemChange(new RemoveVehicle(removedVehicle)); assertThat(firstVisit.getPreviousStandstill()).isNull(); assertThat(lastVisit.getPreviousStandstill()).isNull(); assertThat(solution.getVehicleList()).containsExactly(otherVehicle); mockSolver.verifyVariableChanged(firstVisit, "previousStandstill"); mockSolver.verifyVariableChanged(lastVisit, "previousStandstill"); mockSolver.verifyProblemFactRemoved(removedVehicle); } @Test void fail_fast_if_working_solution_vehicle_list_does_not_contain_working_vehicle() { long removedId = 111L; long wrongId = 222L; PlanningVehicle removedVehicle = PlanningVehicleFactory.testVehicle(removedId); PlanningVehicle wrongVehicle = PlanningVehicleFactory.testVehicle(wrongId); PlanningDepot depot = new PlanningDepot(PlanningLocationFactory.testLocation(1)); VehicleRoutingSolution solution = SolutionFactory.solutionFromVisits( Arrays.asList(wrongVehicle), depot, Collections.emptyList()); // do change RemoveVehicle removeVehicle = new RemoveVehicle(removedVehicle); assertThatIllegalStateException() .isThrownBy(() -> removeVehicle.doChange(solution, new MockProblemChangeDirector())) .withMessageMatching(".*List .*" + wrongId + ".* doesn't contain the working.*" + removedId + ".*"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/change/RemoveVisitTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.change; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory.testVehicle; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit; import org.junit.jupiter.api.Test; import org.optaplanner.test.api.solver.change.MockProblemChangeDirector; import org.optaweb.vehiclerouting.plugin.planner.MockSolver; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; class RemoveVisitTest { @Test void remove_last_visit() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); PlanningVisit removedVisit = testVisit(1); PlanningVisit otherVisit = testVisit(2); solution.getVisitList().add(otherVisit); solution.getVisitList().add(removedVisit); // V -> other -> removed otherVisit.setPreviousStandstill(testVehicle(10)); otherVisit.setNextVisit(removedVisit); removedVisit.setPreviousStandstill(otherVisit); MockSolver mockSolver = MockSolver.build(solution); mockSolver.whenLookingUp(removedVisit).thenReturn(removedVisit); // do change mockSolver.addProblemChange(new RemoveVisit(removedVisit)); mockSolver.verifyEntityRemoved(removedVisit); assertThat(solution.getVisitList()).containsExactly(otherVisit); } @Test void remove_middle_visit() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); PlanningVisit firstVisit = testVisit(1); PlanningVisit middleVisit = testVisit(2); PlanningVisit lastVisit = testVisit(3); solution.getVisitList().add(firstVisit); solution.getVisitList().add(lastVisit); solution.getVisitList().add(middleVisit); // V -> first -> removed -> last firstVisit.setPreviousStandstill(testVehicle(1)); firstVisit.setNextVisit(middleVisit); middleVisit.setPreviousStandstill(firstVisit); middleVisit.setNextVisit(lastVisit); lastVisit.setPreviousStandstill(middleVisit); PlanningVisit removedVisit = testVisit(2); MockSolver mockSolver = MockSolver.build(solution); mockSolver.whenLookingUp(removedVisit).thenReturn(middleVisit); // do change mockSolver.addProblemChange(new RemoveVisit(removedVisit)); mockSolver.verifyVariableChanged(lastVisit, "previousStandstill"); mockSolver.verifyEntityRemoved(removedVisit); assertThat(solution.getVisitList()) .hasSize(2) .containsOnly(firstVisit, lastVisit); // V -> first -> removed -> last assertThat(lastVisit.getPreviousStandstill()).isEqualTo(firstVisit); } @Test void fail_fast_if_working_solution_visit_list_does_not_contain_working_visit() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); long removedId = 111L; PlanningVisit removedVisit = testVisit(removedId); long wrongId = 222L; PlanningVisit wrongVisit = testVisit(wrongId); wrongVisit.setPreviousStandstill(testVisit(10)); removedVisit.setNextVisit(wrongVisit); solution.getVisitList().add(wrongVisit); // do change RemoveVisit removeVisit = new RemoveVisit(removedVisit); assertThatIllegalStateException() .isThrownBy(() -> removeVisit.doChange(solution, new MockProblemChangeDirector())) .withMessageMatching(".*List .*" + wrongId + ".* doesn't contain the working.*" + removedId + ".*"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningLocationFactoryTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; class PlanningLocationFactoryTest { @Test void planning_location_should_have_same_properties_as_domain_location() { long id = 344; double latitude = -20.5; double longitude = 11.7; long distance = 11234; Location location = new Location(id, Coordinates.of(latitude, longitude)); PlanningLocation planningLocation = PlanningLocationFactory.fromDomain(location, otherLocation -> distance); assertThat(planningLocation.getId()).isEqualTo(id); PlanningLocation other = PlanningLocationFactory.testLocation(id + 1); assertThat(planningLocation.distanceTo(other)).isEqualTo(distance); assertThat(planningLocation.angleTo(other)).isNotZero(); } @Test void test_locations_distance_map_should_work() { long distance = 11231; PlanningLocation planningLocation = PlanningLocationFactory.testLocation(0, location -> distance); assertThat(planningLocation.distanceTo(PlanningLocationFactory.testLocation(1))).isEqualTo(distance); } @Test void test_location_without_distance_map_should_throw_exception() { PlanningLocation planningLocation = PlanningLocationFactory.testLocation(0); PlanningLocation otherLocation = PlanningLocationFactory.testLocation(1); assertThatIllegalStateException().isThrownBy(() -> planningLocation.distanceTo(otherLocation)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningLocationTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.testLocation; import java.util.HashMap; import org.assertj.core.data.Offset; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.plugin.planner.DistanceMapImpl; class PlanningLocationTest { @Test void distance_to_location_should_equal_value_in_distance_map() { HashMap distanceMap = new HashMap<>(); long otherId = 321; long millis = 777777; distanceMap.put(otherId, Distance.ofMillis(millis)); Location domainLocation = new Location(1, Coordinates.of(0, 0)); PlanningLocation planningLocation = new PlanningLocation( domainLocation.id(), domainLocation.coordinates().latitude().doubleValue(), domainLocation.coordinates().longitude().doubleValue(), new DistanceMapImpl(distanceMap::get)); assertThat(planningLocation.distanceTo(testLocation(otherId))).isEqualTo(millis); } @Test void angle_from_depot_at_zero_should_be_atan2_of_latitude_longitude() { PlanningLocation center = locationAt(0, 0); assertThat(center.angleTo(locationAt(0, 1))).isZero(); assertThat(center.angleTo(locationAt(0, -1))).isEqualTo(Math.PI); assertThat(center.angleTo(locationAt(1, 0))).isEqualTo(Math.PI / 2); assertThat(center.angleTo(locationAt(-1, 0))).isEqualTo(-Math.PI / 2); assertThat(center.angleTo(locationAt(-Double.MIN_VALUE, -1))).isEqualTo(-Math.PI); assertThat(center.angleTo(locationAt(-0, 1))).isZero(); } @Test void angle_from_depot_on_real_coordinates_should_be_atan2_of_latitude_longitude() { PlanningLocation depot = locationAt(1.77, -10.5); Offset offset = offset(0.05); assertThat(depot.angleTo(locationAt(1.76, -5))).isCloseTo(0, offset).isNegative(); assertThat(depot.angleTo(locationAt(100000, -1))).isCloseTo(Math.PI / 2, offset); } private static PlanningLocation locationAt(double latitude, double longitude) { return new PlanningLocation(0, latitude, longitude, location -> 0); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVehicleFactoryTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVehicleFactory.fromDomain; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleFactory; class PlanningVehicleFactoryTest { @Test void planning_vehicle() { long vehicleId = 2; String name = "not used"; int capacity = 7; Vehicle domainVehicle = VehicleFactory.createVehicle(vehicleId, name, capacity); PlanningVehicle vehicle = fromDomain(domainVehicle); assertThat(vehicle.getId()).isEqualTo(vehicleId); assertThat(vehicle.getCapacity()).isEqualTo(capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVehicleTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.Iterator; import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; class PlanningVehicleTest { @Test void get_future_visits_should_return_an_iterable_that_iterates_over_all_visits() { PlanningVisit visit1 = new PlanningVisit(); PlanningVisit visit2 = new PlanningVisit(); PlanningVisit visit3 = new PlanningVisit(); PlanningVehicle vehicle = new PlanningVehicle(); vehicle.setNextVisit(visit1); visit1.setNextVisit(visit2); visit2.setNextVisit(visit3); Iterable futureVisits = vehicle.getFutureVisits(); assertThat(futureVisits).containsExactly(visit1, visit2, visit3); } @Test void get_future_visits_should_throw_a_NoSuchElementException_when_there_are_no_more_visits() { PlanningVehicle vehicle = new PlanningVehicle(); Iterator futureVisits = vehicle.getFutureVisits().iterator(); assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(futureVisits::next); PlanningVisit visit1 = new PlanningVisit(); PlanningVisit visit2 = new PlanningVisit(); vehicle.setNextVisit(visit1); visit1.setNextVisit(visit2); futureVisits = vehicle.getFutureVisits().iterator(); futureVisits.next(); futureVisits.next(); assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(futureVisits::next); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/domain/PlanningVisitFactoryTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; class PlanningVisitFactoryTest { @Test void visit_should_have_same_id_as_location_and_default_demand() { long id = 4; PlanningLocation location = PlanningLocationFactory.testLocation(id); PlanningVisit visit = PlanningVisitFactory.fromLocation(location); assertThat(visit.getId()).isEqualTo(location.getId()); assertThat(visit.getLocation()).isEqualTo(location); assertThat(visit.getDemand()).isEqualTo(PlanningVisitFactory.DEFAULT_VISIT_DEMAND); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/domain/SolutionFactoryTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.domain; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore; class SolutionFactoryTest { @Test void empty_solution_should_be_empty() { VehicleRoutingSolution solution = SolutionFactory.emptySolution(); assertThat(solution.getVisitList()).isEmpty(); assertThat(solution.getDepotList()).isEmpty(); assertThat(solution.getVehicleList()).isEmpty(); assertThat(solution.getScore()).isEqualTo(HardSoftLongScore.ZERO); } @Test void solution_created_from_vehicles_depot_and_visits_should_be_consistent() { PlanningVehicle vehicle = new PlanningVehicle(); PlanningLocation depotLocation = PlanningLocationFactory.testLocation(1); PlanningDepot depot = new PlanningDepot(depotLocation); PlanningVisit visit = PlanningVisitFactory.testVisit(2); VehicleRoutingSolution solutionWithDepot = SolutionFactory.solutionFromVisits( singletonList(vehicle), depot, singletonList(visit)); assertThat(solutionWithDepot.getVehicleList()).containsExactly(vehicle); assertThat(vehicle.getDepot()).isEqualTo(depot); assertThat(solutionWithDepot.getDepotList()).containsExactly(depot); assertThat(solutionWithDepot.getVisitList()).hasSize(1); assertThat(solutionWithDepot.getVisitList()).containsExactly(visit); assertThat(solutionWithDepot.getVisitList().get(0).getLocation()).isEqualTo(visit.getLocation()); VehicleRoutingSolution solutionWithNoDepot = SolutionFactory.solutionFromVisits( singletonList(vehicle), null, emptyList()); assertThat(solutionWithNoDepot.getDepotList()).isEmpty(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/planner/weight/DepotAngleVisitDifficultyWeightFactoryTest.java ================================================ package org.optaweb.vehiclerouting.plugin.planner.weight; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocationFactory.fromDomain; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.fromLocation; import static org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisitFactory.testVisit; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.plugin.planner.DistanceMapImpl; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningDepot; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningLocation; import org.optaweb.vehiclerouting.plugin.planner.domain.PlanningVisit; import org.optaweb.vehiclerouting.plugin.planner.domain.SolutionFactory; import org.optaweb.vehiclerouting.plugin.planner.domain.VehicleRoutingSolution; import org.optaweb.vehiclerouting.plugin.planner.weight.DepotAngleVisitDifficultyWeightFactory.DepotAngleVisitDifficultyWeight; class DepotAngleVisitDifficultyWeightFactoryTest { private final double depotY = 3.0; private final double depotX = -50.0; private final Map depotDistanceMap = new HashMap<>(); private final PlanningLocation depot; private final VehicleRoutingSolution solution = SolutionFactory.emptySolution(); private final DepotAngleVisitDifficultyWeightFactory weightFactory = new DepotAngleVisitDifficultyWeightFactory(); DepotAngleVisitDifficultyWeightFactoryTest() { Location depotLocation = new Location(0, Coordinates.of(depotY, depotX)); depot = fromDomain(depotLocation, new DistanceMapImpl(depotDistanceMap::get)); solution.getDepotList().add(new PlanningDepot(depot)); } private PlanningLocation location(long id, double latitude, double longitude, long symmetricalDistance) { return location(id, latitude, longitude, symmetricalDistance, symmetricalDistance); } private PlanningLocation location( long id, double latitude, double longitude, long depotToLocation, long locationToDepot) { depotDistanceMap.put(id, Distance.ofMillis(depotToLocation)); Map locationDistanceMap = new HashMap<>(); locationDistanceMap.put(depot.getId(), Distance.ofMillis(locationToDepot)); Location domainLocation = new Location(id, Coordinates.of(latitude, longitude)); return fromDomain(domainLocation, new DistanceMapImpl(locationDistanceMap::get)); } private DepotAngleVisitDifficultyWeight weight(PlanningLocation location) { return weightFactory.createSorterWeight(solution, fromLocation(location)); } @Test void visit_weights_should_be_ordered_by_angle_then_by_distance_then_by_id() { // angle 0 (same as west) distance or ID will decide PlanningLocation center1 = location(1, depotY, depotX, 0); PlanningLocation center2 = location(2, depotY, depotX, 0); PlanningLocation west = location(3, depotY, depotX - 100, 1); // both east (same angle), distance will decide // east1 is closer to depot than east2 PlanningLocation east1 = location(10, depotY, depotX + 37, 100); PlanningLocation east2 = location(20, depotY, depotX + 110.011, 200); // both north (same angle), distance will decide // north1 is closer to depot than north2 PlanningLocation north1 = location(30, depotY + 30.0, depotX, 1); PlanningLocation north2 = location(40, depotY + 60.0, depotX, 2); // all different angle, distance doesn't matter PlanningLocation sw1 = location(50, depotY - 100, depotX - 100, 10_000); PlanningLocation south1 = location(60, depotY - 100, depotX, 10_000); PlanningLocation se1 = location(70, depotY - 100, depotX + 100, 10_000); // E < NE < N < NW < W < SW < S < SE < E (-π → π) assertThat(Stream.of(north1, north2, center1, center2, west, sw1, south1, se1, east1, east2) .map(this::weight) .collect(toList())).isSorted(); assertThat(weight(north1)).isLessThan(weight(north2)); assertThat(weight(north2)).isGreaterThan(weight(north1)); assertThat(weight(center1)).isLessThan(weight(center2)); assertThat(weight(center2)).isGreaterThan(weight(center1)); assertThat(weight(center2)).isEqualByComparingTo(weight(center2)); } @Test void locations_with_asymmetrical_distances_should_be_sorted_by_round_trip_time() { // coordinates only affect angle, distance is stored in the distance map // round-trip: 191 (a < b, although depot→a > depot→b) PlanningLocation a = location(101, depotY, depotX, 101, 90); // round-trip: 200 PlanningLocation b = location(102, depotY, depotX, 100, 100); // round-trip: 250 (c > b, although c→depot < b→depot) PlanningLocation c = location(103, depotY, depotX, 200, 50); assertThat(weight(a)).isLessThan(weight(b)); assertThat(weight(b)).isLessThan(weight(c)); } @Test void equals() { long id = 3; double angle = Math.PI; long distance = 1000; PlanningVisit visit = testVisit(id); DepotAngleVisitDifficultyWeight weight = new DepotAngleVisitDifficultyWeight(visit, angle, distance); assertThat(weight) .isNotEqualTo(null) .isNotEqualTo(this) .isNotEqualTo(new DepotAngleVisitDifficultyWeight(testVisit(id + 1), angle, distance)) .isNotEqualTo(new DepotAngleVisitDifficultyWeight(testVisit(id), -angle, distance)) .isNotEqualTo(new DepotAngleVisitDifficultyWeight(testVisit(id), angle, distance - 1)) .isEqualTo(weight) .isEqualTo(new DepotAngleVisitDifficultyWeight(visit, angle, distance)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/ClearResourceTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import static org.mockito.Mockito.verify; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.service.location.LocationService; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; @ExtendWith(MockitoExtension.class) class ClearResourceTest { @Mock private LocationService locationService; @Mock private VehicleService vehicleService; @InjectMocks private ClearResource clearResource; @Test void clear() { clearResource.clear(); verify(locationService).removeAll(); verify(vehicleService).removeAll(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/DataSetDownloadResourceTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import java.io.IOException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.service.demo.DemoService; @ExtendWith(MockitoExtension.class) class DataSetDownloadResourceTest { @Mock private DemoService demoService; @InjectMocks private DataSetDownloadResource controller; @Test void export() throws IOException { // arrange String msg = "dummy string"; when(demoService.exportDataSet()).thenReturn(msg); // act Response response = controller.exportDataSet(); // assert assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); MultivaluedMap headers = response.getHeaders(); // String.length() works here because the message is ASCII assertThat(headers.getFirst(HttpHeaders.CONTENT_LENGTH)).isEqualTo(msg.length()); assertThat(headers.getFirst(HttpHeaders.CONTENT_TYPE)).isNotNull(); assertThat(headers.getFirst(HttpHeaders.CONTENT_TYPE).toString()) .isEqualToIgnoringWhitespace("text/x-yaml;charset=UTF-8"); assertThat(headers.getFirst(HttpHeaders.CONTENT_DISPOSITION)).isNotNull(); String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION).toString(); assertThat(contentDisposition) .startsWith("attachment;") .containsPattern("; *filename=\".*\\.yaml\""); } @Test void content_length_should_be_number_of_bytes() throws IOException { // Nice illustration of the problem: https://sankhs.com/2016/03/17/content-length-http-headers/ // If the content-length header is less than number of bytes, part of the response body thrown away! // So if we sent "অhello" with content-length: 6, the client (browser) would only present "অhel". // arrange String msg = "অ"; when(demoService.exportDataSet()).thenReturn(msg); // act Response response = controller.exportDataSet(); // assert assertThat(response.getHeaderString(HttpHeaders.CONTENT_LENGTH)).isEqualTo("3"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/DemoResourceTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import static org.mockito.Mockito.verify; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.service.demo.DemoService; @ExtendWith(MockitoExtension.class) class DemoResourceTest { @Mock private DemoService demoService; @InjectMocks private DemoResource demoResource; @Test void demo() { String problemName = "xy"; demoResource.loadDemo(problemName); verify(demoService).loadDemo(problemName); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/LocationResourceTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import static org.mockito.Mockito.verify; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.plugin.rest.model.PortableLocation; import org.optaweb.vehiclerouting.service.location.LocationService; @ExtendWith(MockitoExtension.class) class LocationResourceTest { @Mock private LocationService locationService; @InjectMocks private LocationResource locationResource; @Test void addLocation() { Coordinates coords = Coordinates.of(0.0, 1.0); String description = "new location"; PortableLocation request = new PortableLocation(321, coords.latitude(), coords.longitude(), description); locationResource.addLocation(request); verify(locationService).createLocation(coords, description); } @Test void removeLocation() { locationResource.deleteLocation(9L); verify(locationService).removeLocation(9); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/ServerInfoResourceTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.RoutingProblem; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleFactory; import org.optaweb.vehiclerouting.plugin.rest.model.PortableCoordinates; import org.optaweb.vehiclerouting.plugin.rest.model.RoutingProblemInfo; import org.optaweb.vehiclerouting.plugin.rest.model.ServerInfo; import org.optaweb.vehiclerouting.service.demo.DemoService; import org.optaweb.vehiclerouting.service.region.BoundingBox; import org.optaweb.vehiclerouting.service.region.RegionService; @ExtendWith(MockitoExtension.class) class ServerInfoResourceTest { @Mock private RegionService regionService; @Mock private DemoService demoService; @InjectMocks private ServerInfoResource serverInfoResource; @Test void serverInfo() { // arrange List countryCodes = Arrays.asList("XY", "WZ"); when(regionService.countryCodes()).thenReturn(countryCodes); Coordinates southWest = Coordinates.of(-1.0, -2.0); Coordinates northEast = Coordinates.of(1.0, 2.0); BoundingBox boundingBox = new BoundingBox(southWest, northEast); when(regionService.boundingBox()).thenReturn(boundingBox); Location depot = new Location(1, Coordinates.of(1.0, 7), "Depot"); List visits = Arrays.asList(new Location(2, Coordinates.of(2.0, 9), "Visit")); List vehicles = Arrays.asList(VehicleFactory.testVehicle(1)); String demoName = "Testing problem"; RoutingProblem routingProblem = new RoutingProblem(demoName, vehicles, depot, visits); when(demoService.demos()).thenReturn(Arrays.asList(routingProblem)); // act ServerInfo serverInfo = serverInfoResource.serverInfo(); // assert assertThat(serverInfo.getCountryCodes()).isEqualTo(countryCodes); assertThat(serverInfo.getBoundingBox()).containsExactly( PortableCoordinates.fromCoordinates(southWest), PortableCoordinates.fromCoordinates(northEast)); List demos = serverInfo.getDemos(); Assertions.assertThat(demos).hasSize(1); RoutingProblemInfo demo = demos.get(0); assertThat(demo.getName()).isEqualTo(demoName); assertThat(demo.getVisits()).isEqualTo(visits.size()); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/VehicleResourceTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest; import static org.mockito.Mockito.verify; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; @ExtendWith(MockitoExtension.class) class VehicleResourceTest { @Mock private VehicleService vehicleService; @InjectMocks private VehicleResource vehicleResource; @Test void addVehicle() { vehicleResource.addVehicle(); verify(vehicleService).createVehicle(); } @Test void removeVehicle() { vehicleResource.removeVehicle(11L); verify(vehicleService).removeVehicle(11); } @Test void removeAnyVehicle() { vehicleResource.removeAnyVehicle(); verify(vehicleService).removeAnyVehicle(); } @Test void changeCapacity() { long vehicleId = 2000; int capacity = 50; vehicleResource.changeCapacity(vehicleId, capacity); verify(vehicleService).changeCapacity(vehicleId, capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableCoordinatesTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.math.BigDecimal; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.util.jackson.JacksonAssertions; class PortableCoordinatesTest { @Test void marshal_to_json() { // values are tweaked to enforce rounding to 5 decimal places PortableCoordinates portableCoordinates = new PortableCoordinates( BigDecimal.valueOf(0.123454321), BigDecimal.valueOf(-44.444445111)); JacksonAssertions.assertThat(portableCoordinates).serializedIsEqualToJson("{\"lat\":0.12345,\"lng\":-44.44445}"); } @Test void conversion_from_domain() { Coordinates coordinates = Coordinates.of(0.04687, -88.8889); PortableCoordinates portableCoordinates = PortableCoordinates.fromCoordinates(coordinates); assertThat(portableCoordinates.getLatitude()).isEqualTo(coordinates.latitude()); assertThat(portableCoordinates.getLongitude()).isEqualTo(coordinates.longitude()); assertThatNullPointerException() .isThrownBy(() -> PortableCoordinates.fromCoordinates(null)) .withMessageContaining("coordinates"); } @Test void should_reduce_scale_if_needed() { Coordinates coordinates = Coordinates.of(0.123450001, -88.999999999); Coordinates scaledDown = Coordinates.of(0.12345, -89); PortableCoordinates portableCoordinates = PortableCoordinates.fromCoordinates(coordinates); assertThat(portableCoordinates.getLatitude()).isEqualTo(scaledDown.latitude()); assertThat(portableCoordinates.getLongitude()).isEqualByComparingTo(scaledDown.longitude()); // This would surprisingly fail because actual is -89 and expected is -89.0 // assertThat(portableCoordinates.getLongitude()).isEqualTo(scaledDown.longitude()); } @Test void equals_hashCode_toString() { BigDecimal lat1 = BigDecimal.valueOf(10.0101); BigDecimal lat2 = BigDecimal.valueOf(20.2323); BigDecimal lon1 = BigDecimal.valueOf(-8.7); BigDecimal lon2 = BigDecimal.valueOf(-7.8); PortableCoordinates portableCoordinates = new PortableCoordinates(lat1, lon1); assertThat(portableCoordinates) // equals() .isNotEqualTo(null) .isNotEqualTo(new Coordinates(lat1, lon1)) .isNotEqualTo(new PortableCoordinates(lat1, lon2)) .isNotEqualTo(new PortableCoordinates(lat2, lon1)) .isEqualTo(portableCoordinates) .isEqualTo(new PortableCoordinates(lat1, lon1)) // hasCode() .hasSameHashCodeAs(new PortableCoordinates(lat1, lon1)) // toString() .asString().contains(lat1.toPlainString(), lon1.toPlainString()); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableDistanceTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.util.jackson.JacksonAssertions; class PortableDistanceTest { @Test void marshal_to_json() { Distance distance = Distance.ofMillis(3_661_987); PortableDistance portableDistance = PortableDistance.fromDistance(distance); JacksonAssertions.assertThat(portableDistance).serializedIsEqualToJson("\"1h 1m 2s\""); } @Test void from_distance() { assertThatNullPointerException().isThrownBy(() -> PortableDistance.fromDistance(null)); } @Test void equals_hashCode_toString() { long millis = 173_000; Distance distance = Distance.ofMillis(millis); PortableDistance portableDistance = PortableDistance.fromDistance(distance); assertThat(portableDistance) // equals() .isEqualTo(portableDistance) .isEqualTo(PortableDistance.fromDistance(distance)) .isNotEqualTo(null) .isNotEqualTo(millis) .isNotEqualTo(PortableDistance.fromDistance(Distance.ofMillis(millis - 501))) // hashCode() .hasSameHashCodeAs(PortableDistance.fromDistance(distance)) // toString() .asString().contains("0h 2m 53s"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableErrorMessageTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.service.error.ErrorMessage; import org.optaweb.vehiclerouting.util.jackson.JacksonAssertions; import org.optaweb.vehiclerouting.util.junit.FileContent; class PortableErrorMessageTest { @Test void marshal_to_json(@FileContent("portable-error-message.json") String expectedJson) { String id = "c670dd37-62fb-4e86-95ed-c1f4953aaeaa"; String text = "Error message.\nDetails."; PortableErrorMessage portableErrorMessage = PortableErrorMessage.fromMessage(ErrorMessage.of(id, text)); JacksonAssertions.assertThat(portableErrorMessage).serializedIsEqualToJson(expectedJson); } @Test void factory_method() { String id = "id"; String text = "error"; PortableErrorMessage portableErrorMessage = PortableErrorMessage.fromMessage(ErrorMessage.of(id, text)); assertThat(portableErrorMessage.getId()).isEqualTo(id); assertThat(portableErrorMessage.getText()).isEqualTo(text); } @Test void equals_hashCode_toString() { String id = "1"; String text = "error message"; ErrorMessage message = ErrorMessage.of(id, text); PortableErrorMessage portableErrorMessage = PortableErrorMessage.fromMessage(message); assertThat(portableErrorMessage).isNotEqualTo(null) // equals() .isNotEqualTo(new PortableErrorMessage("", text)) .isNotEqualTo(new PortableErrorMessage(id, "")) .isNotEqualTo(message) .isEqualTo(portableErrorMessage) .isEqualTo(new PortableErrorMessage(id, text)) // hasCode() .hasSameHashCodeAs(new PortableErrorMessage(id, text)) // toString() .asString().contains(id, text); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableLocationTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.math.BigDecimal; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.util.jackson.JacksonAssertions; import org.optaweb.vehiclerouting.util.junit.FileContent; class PortableLocationTest { private final PortableLocation portableLocation = new PortableLocation( 987, BigDecimal.ONE, BigDecimal.TEN, "Some Location"); @Test void marshal_to_json(@FileContent("portable-location.json") String json) { JacksonAssertions.assertThat(portableLocation).serializedIsEqualToJson(json); } @Test void unmarshal_from_json(@FileContent("portable-location.json") String json) { JacksonAssertions.assertThat(json).deserializedIsEqualTo(portableLocation); } @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy( () -> new PortableLocation(1, null, BigDecimal.ZERO, "")); assertThatNullPointerException().isThrownBy( () -> new PortableLocation(1, BigDecimal.ZERO, null, "")); assertThatNullPointerException().isThrownBy( () -> new PortableLocation(1, BigDecimal.ZERO, BigDecimal.ZERO, null)); } @Test void fromLocation() { Location location = new Location(17, Coordinates.of(5.1, -0.0007), "Hello, world!"); PortableLocation portableLocation = PortableLocation.fromLocation(location); assertThat(portableLocation.getId()).isEqualTo(location.id()); assertThat(portableLocation.getLatitude()).isEqualTo(location.coordinates().latitude()); assertThat(portableLocation.getLongitude()).isEqualTo(location.coordinates().longitude()); assertThat(portableLocation.getDescription()).isEqualTo(location.description()); assertThatNullPointerException() .isThrownBy(() -> PortableLocation.fromLocation(null)) .withMessageContaining("location"); } @Test void equals_hashCode_toString() { long id = 123456; String description = "x y"; BigDecimal lat1 = BigDecimal.valueOf(10.0101); BigDecimal lat2 = BigDecimal.valueOf(20.2323); BigDecimal lon1 = BigDecimal.valueOf(-8.7); BigDecimal lon2 = BigDecimal.valueOf(-7.8); PortableLocation portableLocation = new PortableLocation(id, lat1, lon1, description); assertThat(portableLocation) // equals() .isNotEqualTo(null) .isNotEqualTo(new Location(id, new Coordinates(lat1, lon1))) .isNotEqualTo(new PortableLocation(id + 1, lat1, lon1, description)) .isNotEqualTo(new PortableLocation(id, lat1, lon2, description)) .isNotEqualTo(new PortableLocation(id, lat2, lon1, description)) .isNotEqualTo(new PortableLocation(id, lat1, lon1, "y x")) .isEqualTo(portableLocation) .isEqualTo(new PortableLocation(id, lat1, lon1, description)) // hasCode() .hasSameHashCodeAs(new PortableLocation(id, lat1, lon1, description)) // toString() .asString() .contains( String.valueOf(id), lat1.toPlainString(), lon1.toPlainString(), description); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRouteTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static java.util.Arrays.asList; import static org.optaweb.vehiclerouting.plugin.rest.model.PortableCoordinates.fromCoordinates; import static org.optaweb.vehiclerouting.plugin.rest.model.PortableLocation.fromLocation; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.util.jackson.JacksonAssertions; import org.optaweb.vehiclerouting.util.junit.FileContent; class PortableRouteTest { @Test void marshal_to_json(@FileContent("portable-route.json") String expectedJson) { PortableVehicle vehicle = new PortableVehicle(13, "Vehicle", 45317); PortableLocation depot = visit(8, 42.6501218, -71.8835449, "Test depot"); PortableLocation visit1 = visit(100, 42.7066596, -72.4934873, "Visit 1"); PortableLocation visit2 = visit(200, 42.5543343, -71.4438280, "Visit 2"); PortableRoute portableRoute = new PortableRoute( vehicle, depot, asList(visit1, visit2), asList( asList( coordinates(42.65005, -71.88522), coordinates(42.64997, -71.88527)), asList( coordinates(42.64994, -71.88537), coordinates(42.64994, -71.88542)))); JacksonAssertions.assertThat(portableRoute).serializedIsEqualToJson(expectedJson); } private static PortableLocation visit(long id, double latitude, double longitude, String description) { return fromLocation(new Location(id, Coordinates.of(latitude, longitude), description)); } private static PortableCoordinates coordinates(double latitude, double longitude) { return fromCoordinates(Coordinates.of(latitude, longitude)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableRoutingPlanFactoryTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.Route; import org.optaweb.vehiclerouting.domain.RouteWithTrack; import org.optaweb.vehiclerouting.domain.RoutingPlan; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleFactory; class PortableRoutingPlanFactoryTest { @Test void portable_routing_plan_empty() { PortableRoutingPlan portablePlan = PortableRoutingPlanFactory.fromRoutingPlan(RoutingPlan.empty()); assertThat(portablePlan.getDistance()).isEqualTo(PortableDistance.fromDistance(Distance.ZERO)); assertThat(portablePlan.getVehicles()).isEmpty(); assertThat(portablePlan.getDepot()).isNull(); assertThat(portablePlan.getRoutes()).isEmpty(); } @Test void portable_routing_plan_with_two_routes() { // arrange final Coordinates coordinates1 = Coordinates.of(0.0, 0.1); final Coordinates coordinates2 = Coordinates.of(2.0, -0.2); final Coordinates coordinates3 = Coordinates.of(3.3, -3.3); final Coordinates checkpoint12 = Coordinates.of(12, 12); final Coordinates checkpoint21 = Coordinates.of(21, 21); final Coordinates checkpoint13 = Coordinates.of(13, 13); final Coordinates checkpoint31 = Coordinates.of(31, 31); List segment12 = asList(coordinates1, checkpoint12, coordinates2); List segment21 = asList(coordinates2, checkpoint21, coordinates1); List segment13 = asList(coordinates1, checkpoint13, coordinates3); List segment31 = asList(coordinates3, checkpoint31, coordinates1); final Location location1 = new Location(1, coordinates1); final Location location2 = new Location(2, coordinates2); final Location location3 = new Location(3, coordinates3); final Distance distance = Distance.ofMillis(5); final Vehicle vehicle1 = VehicleFactory.createVehicle(1, "Vehicle 1", 100); final Vehicle vehicle2 = VehicleFactory.createVehicle(2, "Vehicle 2", 200); RouteWithTrack route1 = new RouteWithTrack( new Route(vehicle1, location1, singletonList(location2)), asList(segment12, segment21)); RouteWithTrack route2 = new RouteWithTrack( new Route(vehicle2, location1, singletonList(location3)), asList(segment13, segment31)); RoutingPlan routingPlan = new RoutingPlan( distance, asList(vehicle1, vehicle2), location1, asList(location2, location3), asList(route1, route2)); // act PortableRoutingPlan portableRoutingPlan = PortableRoutingPlanFactory.fromRoutingPlan(routingPlan); // assert // -- plan.distance assertThat(portableRoutingPlan.getDistance()).isEqualTo(PortableDistance.fromDistance(distance)); // -- plan.depot assertThat(portableRoutingPlan.getDepot()).isEqualTo(PortableLocation.fromLocation(location1)); // -- plan.visits assertThat(portableRoutingPlan.getVisits()).containsExactlyInAnyOrder( PortableLocation.fromLocation(location2), PortableLocation.fromLocation(location3)); // -- plan.routes assertThat(portableRoutingPlan.getRoutes()).hasSize(2); // -- plan.vehicles assertThat(portableRoutingPlan.getVehicles()).containsExactlyInAnyOrder( PortableVehicle.fromVehicle(vehicle1), PortableVehicle.fromVehicle(vehicle2)); // -- plan.routes[1] PortableRoute portableRoute1 = portableRoutingPlan.getRoutes().get(0); assertThat(portableRoute1.getVehicle()).isEqualTo(PortableVehicle.fromVehicle(vehicle1)); assertThat(portableRoute1.getDepot()).isEqualTo(PortableLocation.fromLocation(location1)); assertThat(portableRoute1.getVisits()).containsExactly( PortableLocation.fromLocation(location2)); assertThat(portableRoute1.getTrack()).hasSize(2); assertThat(portableRoute1.getTrack().get(0)).containsExactly( PortableCoordinates.fromCoordinates(location1.coordinates()), PortableCoordinates.fromCoordinates(checkpoint12), PortableCoordinates.fromCoordinates(location2.coordinates())); assertThat(portableRoute1.getTrack().get(1)).containsExactly( PortableCoordinates.fromCoordinates(location2.coordinates()), PortableCoordinates.fromCoordinates(checkpoint21), PortableCoordinates.fromCoordinates(location1.coordinates())); // -- plan.routes[2] PortableRoute portableRoute2 = portableRoutingPlan.getRoutes().get(1); assertThat(portableRoute2.getVehicle()).isEqualTo(PortableVehicle.fromVehicle(vehicle2)); assertThat(portableRoute2.getDepot()).isEqualTo(PortableLocation.fromLocation(location1)); assertThat(portableRoute2.getVisits()).containsExactly( PortableLocation.fromLocation(location3)); assertThat(portableRoute2.getTrack()).hasSize(2); assertThat(portableRoute2.getTrack().get(0)).containsExactly( PortableCoordinates.fromCoordinates(location1.coordinates()), PortableCoordinates.fromCoordinates(checkpoint13), PortableCoordinates.fromCoordinates(location3.coordinates())); assertThat(portableRoute2.getTrack().get(1)).containsExactly( PortableCoordinates.fromCoordinates(location3.coordinates()), PortableCoordinates.fromCoordinates(checkpoint31), PortableCoordinates.fromCoordinates(location1.coordinates())); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/rest/model/PortableVehicleTest.java ================================================ package org.optaweb.vehiclerouting.plugin.rest.model; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.VehicleFactory; import org.optaweb.vehiclerouting.util.jackson.JacksonAssertions; class PortableVehicleTest { @Test void marshall_to_json() { long id = 321; String name = "Pink: {XY-123} \"B\""; int capacity = 78; PortableVehicle portableVehicle = new PortableVehicle(id, name, capacity); String jsonTemplate = "{\"id\":%d,\"name\":\"%s\",\"capacity\":%d}"; String expected = String.format(jsonTemplate, id, name.replaceAll("\"", "\\\\\""), capacity); JacksonAssertions.assertThat(portableVehicle).serializedIsEqualToJson(expected); } @Test void constructor_params_must_not_be_null() { assertThatNullPointerException().isThrownBy(() -> new PortableVehicle(1, null, 2)); } @Test void fromVehicle() { long id = 321; String name = "Pink XY-123 B"; int capacity = 31; PortableVehicle portableVehicle = PortableVehicle.fromVehicle(VehicleFactory.createVehicle(id, name, capacity)); assertThat(portableVehicle.getId()).isEqualTo(id); assertThat(portableVehicle.getName()).isEqualTo(name); assertThat(portableVehicle.getCapacity()).isEqualTo(capacity); assertThatNullPointerException() .isThrownBy(() -> PortableVehicle.fromVehicle(null)) .withMessageContaining("vehicle"); } @Test void equals_hashCode_toString() { long id = 123456; String name = "x y"; int capacity = 444111; PortableVehicle portableVehicle = new PortableVehicle(id, name, capacity); assertThat(portableVehicle) // equals() .isNotEqualTo(null) .isNotEqualTo(VehicleFactory.createVehicle(id, name, capacity)) .isNotEqualTo(new PortableVehicle(id + 1, name, capacity)) .isNotEqualTo(new PortableVehicle(id, name + "z", capacity)) .isNotEqualTo(new PortableVehicle(id, name, capacity + 1)) .isEqualTo(portableVehicle) .isEqualTo(new PortableVehicle(id, name, capacity)) // hasCode() .hasSameHashCodeAs(new PortableVehicle(id, name, capacity)) // toString() .asString() .contains( String.valueOf(id), name, String.valueOf(capacity)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/routing/AirDistanceRouterTest.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.service.region.BoundingBox; class AirDistanceRouterTest { @Test void travel_time_should_be_distance_divided_by_speed() { AirDistanceRouter router = new AirDistanceRouter(); Coordinates from = Coordinates.of(0, 0); Coordinates to = Coordinates.of(3, 4); // √(3² + 4²) = 5 long travelTimeMillis = router.travelTimeMillis(from, to); assertThat(travelTimeMillis).isEqualTo((long) (5 * AirDistanceRouter.KILOMETERS_PER_DEGREE / AirDistanceRouter.TRAVEL_SPEED_KPH * AirDistanceRouter.MILLIS_IN_ONE_HOUR)); } @Test void bounding_box_is_the_whole_globe() { BoundingBox bounds = new AirDistanceRouter().getBounds(); assertThat(bounds.getSouthWest()).isEqualTo(Coordinates.of(-90, -180)); assertThat(bounds.getNorthEast()).isEqualTo(Coordinates.of(90, 180)); } @Test void path_from_a_to_b_should_be_the_line_ab() { AirDistanceRouter router = new AirDistanceRouter(); Coordinates from = Coordinates.of(0, 0); Coordinates to = Coordinates.of(3, 4); assertThat(router.getPath(from, to)).containsExactly(from, to); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/routing/GraphHopperIntegrationTest.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import static org.assertj.core.api.Assertions.assertThatCode; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import com.graphhopper.GraphHopper; import com.graphhopper.config.Profile; class GraphHopperIntegrationTest { private static final String OSM_PBF = "planet_12.032,53.0171_12.1024,53.0491.osm.pbf"; @Test void graphhopper_should_import_and_load_osm_file_successfully(@TempDir Path tempDir) { Path graphhopperDir = tempDir.resolve("graphhopper"); GraphHopper graphHopper = new GraphHopper(); graphHopper.setGraphHopperLocation(graphhopperDir.toString()); graphHopper.setOSMFile(GraphHopperIntegrationTest.class.getResource(OSM_PBF).getFile()); graphHopper.setProfiles(new Profile(Constants.GRAPHHOPPER_PROFILE).setVehicle("car").setWeighting("fastest")); assertThatCode(graphHopper::importOrLoad).doesNotThrowAnyException(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/routing/GraphHopperRouterTest.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.service.region.BoundingBox; import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopper; import com.graphhopper.ResponsePath; import com.graphhopper.storage.BaseGraph; import com.graphhopper.util.PointList; import com.graphhopper.util.shapes.BBox; @ExtendWith(MockitoExtension.class) class GraphHopperRouterTest { private final PointList pointList = new PointList(); private final Coordinates from = Coordinates.of(-Double.MIN_VALUE, Double.MIN_VALUE); private final Coordinates to = Coordinates.of(Double.MAX_VALUE, -Double.MAX_VALUE); @Mock private GraphHopper graphHopper; @Mock private GHResponse ghResponse; @Mock private ResponsePath pathWrapper; @Mock private BaseGraph baseGraph; private void whenRouteReturnResponse() { when(graphHopper.route(any(GHRequest.class))).thenReturn(ghResponse); } private void whenBestReturnPath() { when(ghResponse.getBest()).thenReturn(pathWrapper); } @Test void travel_time_should_return_graphhopper_time() { // arrange whenRouteReturnResponse(); whenBestReturnPath(); long travelTimeMillis = 135 * 60 * 60 * 1000; when(pathWrapper.getTime()).thenReturn(travelTimeMillis); // act & assert assertThat(new GraphHopperRouter(graphHopper).travelTimeMillis(from, to)).isEqualTo(travelTimeMillis); } @Test void getDistance_should_throw_exception_when_no_route_exists() { // arrange whenRouteReturnResponse(); when(ghResponse.hasErrors()).thenReturn(true); when(ghResponse.getErrors()).thenReturn(Collections.singletonList(new RuntimeException())); GraphHopperRouter graphHopperRouter = new GraphHopperRouter(graphHopper); // act & assert assertThatThrownBy(() -> graphHopperRouter.travelTimeMillis(from, to)) .isNotInstanceOf(NullPointerException.class) .isInstanceOf(RuntimeException.class) .hasMessageContaining("No route"); } @Test void getRoute_should_return_graphhopper_route() { // arrange whenRouteReturnResponse(); whenBestReturnPath(); when(pathWrapper.getPoints()).thenReturn(pointList); Coordinates coordinates1 = Coordinates.of(1, 1); Coordinates coordinates2 = Coordinates.of(Math.E, Math.PI); Coordinates coordinates3 = Coordinates.of(0.1, 1.0 / 3.0); pointList.add(coordinates1.latitude().doubleValue(), coordinates1.longitude().doubleValue()); pointList.add(coordinates2.latitude().doubleValue(), coordinates2.longitude().doubleValue()); pointList.add(coordinates3.latitude().doubleValue(), coordinates3.longitude().doubleValue()); // act & assert List route = new GraphHopperRouter(graphHopper).getPath(from, to); assertThat(route).containsExactly( coordinates1, coordinates2, coordinates3); } @Test void should_return_graphHopper_bounds() { when(graphHopper.getBaseGraph()).thenReturn(baseGraph); double minLat_Y = -90; double minLon_X = -180; double maxLat_Y = 90; double maxLon_X = 180; BBox bbox = new BBox(minLon_X, maxLon_X, minLat_Y, maxLat_Y); when(baseGraph.getBounds()).thenReturn(bbox); BoundingBox boundingBox = new GraphHopperRouter(graphHopper).getBounds(); assertThat(boundingBox.getSouthWest()).isEqualTo(Coordinates.of(minLat_Y, minLon_X)); assertThat(boundingBox.getNorthEast()).isEqualTo(Coordinates.of(maxLat_Y, maxLon_X)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/plugin/routing/RoutingConfigTest.java ================================================ package org.optaweb.vehiclerouting.plugin.routing; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class RoutingConfigTest { @Test void should_throw_exception_when_url_is_malformed() { Path osmFile = Mockito.mock(Path.class); String malformedUrl = "x+y"; assertThatExceptionOfType(RoutingEngineException.class) .isThrownBy(() -> RoutingConfig.downloadOsmFile(malformedUrl, osmFile)) .withMessageContaining("malformed"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/demo/DemoServiceTest.java ================================================ package org.optaweb.vehiclerouting.service.demo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.RoutingProblem; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleData; import org.optaweb.vehiclerouting.domain.VehicleFactory; import org.optaweb.vehiclerouting.service.demo.dataset.DataSetMarshaller; import org.optaweb.vehiclerouting.service.location.LocationRepository; import org.optaweb.vehiclerouting.service.location.LocationService; import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; @ExtendWith(MockitoExtension.class) class DemoServiceTest { @Mock private RoutingProblemList routingProblems; @Mock private LocationService locationService; @Mock private LocationRepository locationRepository; @Mock private VehicleService vehicleService; @Mock private VehicleRepository vehicleRepository; @Mock private DataSetMarshaller dataSetMarshaller; @InjectMocks private DemoService demoService; @Captor private ArgumentCaptor routingProblemCaptor; private final String problemName = "Testing problem"; private final List vehicles = Arrays.asList( VehicleFactory.vehicleData("v1", 10), VehicleFactory.vehicleData("v2", 10)); private final Location depot = new Location(1, Coordinates.of(1.0, 7), "Depot"); private final List visits = Arrays.asList(new Location(2, Coordinates.of(2.0, 9), "Visit")); private final RoutingProblem routingProblem = new RoutingProblem(problemName, vehicles, depot, visits); @Test void demos_should_return_routing_problems() { // arrange when(routingProblems.all()).thenReturn(Arrays.asList(routingProblem)); // act Collection problems = demoService.demos(); // assert assertThat(problems).containsExactly(routingProblem); } @Test void loadDemo() { // arrange Location location = new Location(10, Coordinates.of(1, 2)); when(routingProblems.byName(problemName)).thenReturn(routingProblem); when(locationService.createLocation(any(Coordinates.class), anyString())).thenReturn(Optional.of(location)); // act demoService.loadDemo(problemName); // assert verify(locationService, times(routingProblem.visits().size() + 1)) .createLocation(any(Coordinates.class), anyString()); verify(vehicleService, times(routingProblem.vehicles().size())) .createVehicle(any(VehicleData.class)); } @Test void retry_when_adding_location_fails() { when(routingProblems.byName(problemName)).thenReturn(routingProblem); when(locationService.createLocation(any(Coordinates.class), anyString())).thenReturn(Optional.empty()); assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> demoService.loadDemo(problemName)) .withMessageContaining(depot.coordinates().toString()); verify(locationService, times(DemoService.MAX_TRIES)).createLocation(any(Coordinates.class), anyString()); } @Test void export_should_marshal_routing_plans_with_locations_and_vehicles_from_repository() { Location depot = new Location(0, Coordinates.of(1.0, 2.0), "Depot"); Location visit1 = new Location(1, Coordinates.of(11.0, 22.0), "Visit 1"); Location visit2 = new Location(2, Coordinates.of(22.0, 33.0), "Visit 2"); Vehicle vehicle1 = VehicleFactory.createVehicle(11, "Vehicle 1", 100); Vehicle vehicle2 = VehicleFactory.createVehicle(12, "Vehicle 2", 200); when(locationRepository.locations()).thenReturn(Arrays.asList(depot, visit1, visit2)); when(vehicleRepository.vehicles()).thenReturn(Arrays.asList(vehicle1, vehicle2)); demoService.exportDataSet(); RoutingProblem routingProblem = verifyAndCaptureMarshalledProblem(); assertThat(routingProblem.name()).isNotNull(); assertThat(routingProblem.depot()).contains(depot); assertThat(routingProblem.visits()).containsExactly(visit1, visit2); assertThat(routingProblem.vehicles()).containsExactly(vehicle1, vehicle2); } @Test void export_should_marshal_empty_routing_plan_when_repositories_empty() { String result = "empty routing plan"; when(locationRepository.locations()).thenReturn(Collections.emptyList()); when(vehicleRepository.vehicles()).thenReturn(Collections.emptyList()); when(dataSetMarshaller.marshal(any())).thenReturn(result); assertThat(demoService.exportDataSet()).isEqualTo(result); RoutingProblem routingProblem = verifyAndCaptureMarshalledProblem(); assertThat(routingProblem.name()).isNotNull(); assertThat(routingProblem.depot()).isEmpty(); assertThat(routingProblem.visits()).isEmpty(); assertThat(routingProblem.vehicles()).isEmpty(); } private RoutingProblem verifyAndCaptureMarshalledProblem() { verify(dataSetMarshaller).marshal(routingProblemCaptor.capture()); return routingProblemCaptor.getValue(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/demo/RoutingProblemListTest.java ================================================ package org.optaweb.vehiclerouting.service.demo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.util.Collections; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.RoutingProblem; import org.optaweb.vehiclerouting.domain.VehicleData; class RoutingProblemListTest { @Test void should_validate_constructor_arguments() { assertThatNullPointerException().isThrownBy(() -> new RoutingProblemList(null)); } @Test void should_fail_on_duplicate_problem_names() { String name = "DUPLICATE_NAME"; RoutingProblem p1 = new RoutingProblem(name, Collections.emptyList(), null, Collections.emptyList()); RoutingProblem p2 = new RoutingProblem(name, Collections.emptyList(), null, Collections.emptyList()); assertThatIllegalStateException() .isThrownBy(() -> new RoutingProblemList(Stream.of(p1, p2))) .withMessageContaining(name); } @Test void all_by_name_should_return_expected_problems() { List vehicles = Collections.emptyList(); Location depot = new Location(0, Coordinates.of(10, -20)); List visits = Collections.emptyList(); String name1 = "Problem A"; String name2 = "Problem B"; RoutingProblemList routingProblemList = new RoutingProblemList(Stream.of( new RoutingProblem(name1, vehicles, depot, visits), new RoutingProblem(name2, vehicles, depot, visits))); assertThat(routingProblemList.all()).extracting("name").containsExactlyInAnyOrder(name1, name2); assertThat(routingProblemList.byName(name1).name()).isEqualTo(name1); assertThatIllegalArgumentException().isThrownBy(() -> routingProblemList.byName("Unknown problem")); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/demo/dataset/DataSetMarshallerTest.java ================================================ package org.optaweb.vehiclerouting.service.demo.dataset; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.optaweb.vehiclerouting.service.demo.dataset.DataSetMarshaller.toDataSet; import static org.optaweb.vehiclerouting.service.demo.dataset.DataSetMarshaller.toDomain; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.LocationData; import org.optaweb.vehiclerouting.domain.RoutingProblem; import org.optaweb.vehiclerouting.domain.VehicleData; import org.optaweb.vehiclerouting.domain.VehicleFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; class DataSetMarshallerTest { @Test void unmarshal_data_set() throws IOException { DataSet dataSet; try (InputStream inputStream = DataSetMarshallerTest.class.getResourceAsStream("test-belgium.yaml")) { dataSet = new DataSetMarshaller().unmarshalToDataSet( new InputStreamReader(inputStream, StandardCharsets.UTF_8)); } assertThat(dataSet).isNotNull(); assertThat(dataSet.getName()).isEqualTo("Belgium test"); assertThat(dataSet.getDepot()).isNotNull(); assertThat(dataSet.getDepot().getLabel()).isEqualTo("Brussels"); assertThat(dataSet.getDepot().getLatitude()).isEqualTo(50.85); assertThat(dataSet.getDepot().getLongitude()).isEqualTo(4.35); assertThat(dataSet.getVisits()) .extracting("label") .containsExactlyInAnyOrder("Aalst", "Châtelet", "La Louvière", "Sint-Niklaas", "Ypres"); assertThat(dataSet.getVehicles()) .extracting(dataSetVehicle -> dataSetVehicle.name, dataSetVehicle -> dataSetVehicle.capacity) .containsExactlyInAnyOrder( tuple("vehicle 1", 10), tuple("vehicle 2", 12), tuple("vehicle 3", 1_000_000)); } @Test void marshal_data_set() { DataSet dataSet = new DataSet(); String name = "Test data set"; dataSet.setName(name); DataSetLocation depot = new DataSetLocation("Depot", -1.1, -9.9); DataSetLocation location1 = new DataSetLocation("Location 1", 1.0, 0.1); DataSetLocation location2 = new DataSetLocation("Location 2", 2.0, 0.2); dataSet.setDepot(depot); dataSet.setVisits(Arrays.asList(location1, location2)); DataSetVehicle vehicle1 = new DataSetVehicle("Vehicle 1", 123); DataSetVehicle vehicle2 = new DataSetVehicle("Vehicle 2", 222); dataSet.setVehicles(Arrays.asList(vehicle1, vehicle2)); String yaml = new DataSetMarshaller().marshal(dataSet); assertThat(yaml) .contains("name: \"" + name) .contains( depot.getLabel(), location1.getLabel(), location2.getLabel(), vehicle1.name, vehicle2.name, String.valueOf(vehicle1.capacity), String.valueOf(vehicle2.capacity)); } @Test void should_rethrow_exception_from_object_mapper() throws IOException { ObjectMapper objectMapper = mock(ObjectMapper.class); when(objectMapper.readValue(any(Reader.class), eq(DataSet.class))).thenThrow(IOException.class); assertThatIllegalStateException() .isThrownBy(() -> new DataSetMarshaller(objectMapper).unmarshalToDataSet(mock(Reader.class))) .withRootCauseExactlyInstanceOf(IOException.class); when(objectMapper.writeValueAsString(any(DataSet.class))).thenThrow(JsonProcessingException.class); assertThatIllegalStateException() .isThrownBy(() -> new DataSetMarshaller(objectMapper).marshal(new DataSet())) .withRootCauseExactlyInstanceOf(JsonProcessingException.class); } @Test void location_conversion() { double lat = -1.0; double lng = 50.2; String description = "some location"; // domain -> data set DataSetLocation dataSetLocation = toDataSet(new LocationData(Coordinates.of(lat, lng), description)); assertThat(dataSetLocation.getLatitude()).isEqualTo(lat); assertThat(dataSetLocation.getLongitude()).isEqualTo(lng); assertThat(dataSetLocation.getLabel()).isEqualTo(description); // data set -> domain LocationData location = toDomain(dataSetLocation); assertThat(location).isEqualTo(new LocationData(Coordinates.of(lat, lng), description)); } @Test void routing_problem_conversion() { VehicleData vehicle = VehicleFactory.vehicleData("vehicle", 10); List vehicles = Arrays.asList(vehicle); LocationData depot = new LocationData(Coordinates.of(60.1, 5.78), "Depot"); LocationData visit = new LocationData(Coordinates.of(1.06, 8.75), "Visit"); List visits = Arrays.asList(visit); String name = "some data set"; // domain -> data set DataSet dataSet = toDataSet(new RoutingProblem(name, vehicles, depot, visits)); assertThat(dataSet.getName()).isEqualTo(name); assertThat(dataSet.getVehicles()).hasSameSizeAs(vehicles); assertThat(toDomain(dataSet.getVehicles().get(0))).isEqualTo(vehicle); assertThat(toDomain(dataSet.getDepot())).isEqualTo(depot); assertThat(dataSet.getVisits()).hasSameSizeAs(visits); assertThat(toDomain(dataSet.getVisits().get(0))).isEqualTo(visit); // data set -> domain RoutingProblem routingProblem = toDomain(dataSet); assertThat(routingProblem.name()).isEqualTo(name); assertThat(routingProblem.vehicles()).containsExactly(vehicle); assertThat(routingProblem.depot()).contains(depot); assertThat(routingProblem.visits()).containsExactly(visit); } @Test void should_convert_empty_data_set_correctly() { DataSet emptyDataSet = new DataSet(); RoutingProblem routingProblem = toDomain(emptyDataSet); assertThat(routingProblem.name()).isEmpty(); assertThat(routingProblem.depot()).isEmpty(); assertThat(routingProblem.vehicles()).isEmpty(); assertThat(routingProblem.visits()).isEmpty(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/distance/DistanceMatrixImplTest.java ================================================ package org.optaweb.vehiclerouting.service.distance; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import java.math.BigDecimal; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.service.location.DistanceMatrixRow; @ExtendWith(MockitoExtension.class) class DistanceMatrixImplTest { @Mock private DistanceCalculator distanceCalculator; @InjectMocks private DistanceMatrixImpl distanceMatrix; @Test void should_calculate_distance_map() { DistanceMatrixImpl distanceMatrix = new DistanceMatrixImpl(new MockDistanceCalculator()); Location l0 = location(100, 0); Location l1 = location(111, 1); Location l9neg = location(321, -9); DistanceMatrixRow matrixRow0 = distanceMatrix.addLocation(l0); // distance to self assertThat(matrixRow0.distanceTo(l0.id())).isEqualTo(Distance.ZERO); assertThat(distanceMatrix.distance(l0, l0)).isEqualTo(Distance.ZERO); // distance to not yet registered location assertThatIllegalArgumentException().isThrownBy(() -> matrixRow0.distanceTo(l1.id())); assertThatIllegalArgumentException().isThrownBy(() -> distanceMatrix.distance(l0, l1)); assertThatIllegalArgumentException().isThrownBy(() -> distanceMatrix.distance(l1, l0)); assertThatIllegalArgumentException().isThrownBy(() -> distanceMatrix.distance(l1, l1)); DistanceMatrixRow matrixRow1 = distanceMatrix.addLocation(l1); // distance to self assertThat(matrixRow1.distanceTo(l1.id())).isEqualTo(Distance.ZERO); assertThat(distanceMatrix.distance(l1, l1)).isEqualTo(Distance.ZERO); // distance 0 <-> 1 assertThat(matrixRow1.distanceTo(l0.id())).isEqualTo(Distance.ofMillis(1)); assertThat(distanceMatrix.distance(l0, l1)).isEqualTo(Distance.ofMillis(1)); assertThat(matrixRow0.distanceTo(l1.id())).isEqualTo(Distance.ofMillis(1)); assertThat(distanceMatrix.distance(l1, l0)).isEqualTo(Distance.ofMillis(1)); DistanceMatrixRow matrixRow9 = distanceMatrix.addLocation(l9neg); // distances -9 -> {0, 1} assertThat(matrixRow9.distanceTo(l0.id())).isEqualTo(Distance.ofMillis(9)); assertThat(distanceMatrix.distance(l9neg, l0)).isEqualTo(Distance.ofMillis(9)); assertThat(matrixRow9.distanceTo(l1.id())).isEqualTo(Distance.ofMillis(10)); assertThat(distanceMatrix.distance(l9neg, l1)).isEqualTo(Distance.ofMillis(10)); // distances {0, 1} -> -9 assertThat(matrixRow0.distanceTo(l9neg.id())).isEqualTo(Distance.ofMillis(9)); assertThat(distanceMatrix.distance(l0, l9neg)).isEqualTo(Distance.ofMillis(9)); assertThat(matrixRow1.distanceTo(l9neg.id())).isEqualTo(Distance.ofMillis(10)); assertThat(distanceMatrix.distance(l1, l9neg)).isEqualTo(Distance.ofMillis(10)); // clear the map assertThat(distanceMatrix.dimension()).isEqualTo(3); distanceMatrix.clear(); assertThat(distanceMatrix.dimension()).isZero(); Location l500 = location(500, 500); DistanceMatrixRow matrixRow500 = distanceMatrix.addLocation(l500); assertThatIllegalArgumentException().isThrownBy(() -> matrixRow500.distanceTo(l0.id())); assertThatIllegalArgumentException().isThrownBy(() -> distanceMatrix.distance(l500, l0)); assertThatIllegalArgumentException().isThrownBy(() -> matrixRow9.distanceTo(l500.id())); assertThatIllegalArgumentException().isThrownBy(() -> distanceMatrix.distance(l9neg, l500)); } @Test void should_calculate_distance_only_once() { Location l1 = location(100, -1); Location l2 = location(111, 20); long dist12 = 12; long dist21 = 21; when(distanceCalculator.travelTimeMillis(l1.coordinates(), l2.coordinates())).thenReturn(dist12); when(distanceCalculator.travelTimeMillis(l2.coordinates(), l1.coordinates())).thenReturn(dist21); // No calculation for the first location. distanceMatrix.addLocation(l1); verifyNoInteractions(distanceCalculator); // Calculation happens for the first time. distanceMatrix.addLocation(l2); verify(distanceCalculator).travelTimeMillis(l1.coordinates(), l2.coordinates()); verify(distanceCalculator).travelTimeMillis(l2.coordinates(), l1.coordinates()); // No calculation if the matrix is already populated. DistanceMatrixRow row21 = distanceMatrix.addLocation(l2); assertThat(row21.distanceTo(l1.id())).isEqualTo(Distance.ofMillis(dist21)); DistanceMatrixRow row12 = distanceMatrix.addLocation(l1); assertThat(row12.distanceTo(l2.id())).isEqualTo(Distance.ofMillis(dist12)); verifyNoMoreInteractions(distanceCalculator); } @Test void should_remove_distance_row_from_matrix_and_repository_when_location_removed() { // arrange Location l1 = location(1, 1); Location l2 = location(2, 2); when(distanceCalculator.travelTimeMillis(l1.coordinates(), l2.coordinates())).thenThrow(new RoutingException("dummy")); distanceMatrix.addLocation(l1); assertThatExceptionOfType(RoutingException.class).isThrownBy(() -> distanceMatrix.addLocation(l2)); assertThat(distanceMatrix.dimension()).isEqualTo(1); // act & assert distanceMatrix.removeLocation(l1); assertThat(distanceMatrix.dimension()).isZero(); distanceMatrix.addLocation(l2); assertThat(distanceMatrix.dimension()).isEqualTo(1); } @Test void get_distance_after_put() { Location from = location(1, 1); Location to = location(2, 2); Distance distance = Distance.ofMillis(2000); distanceMatrix.put(from, to, distance); assertThat(distanceMatrix.distance(from, to)).isEqualTo(distance); verifyNoInteractions(distanceCalculator); } private static Location location(long id, int longitude) { return new Location(id, new Coordinates(BigDecimal.ZERO, BigDecimal.valueOf(longitude))); } private static class MockDistanceCalculator implements DistanceCalculator { @Override public long travelTimeMillis(Coordinates from, Coordinates to) { // imagine 1D space (all locations on equator) return (long) Math.abs(to.longitude().doubleValue() - from.longitude().doubleValue()); } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/error/ErrorListenerTest.java ================================================ package org.optaweb.vehiclerouting.service.error; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; import javax.enterprise.event.Event; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class ErrorListenerTest { @Captor private ArgumentCaptor argumentCaptor; @Test void should_pass_error_message_to_consumer(@Mock Event errorMessageEvent) { // arrange String text = "error"; ErrorListener errorListener = new ErrorListener(errorMessageEvent); // act errorListener.onErrorEvent(new ErrorEvent(this, text)); // assert verify(errorMessageEvent).fire(argumentCaptor.capture()); ErrorMessage capturedMessage = argumentCaptor.getValue(); assertThat(capturedMessage.text).isEqualTo(text); assertThat(capturedMessage.id).isNotNull(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/location/LocationServiceIntegrationTest.java ================================================ package org.optaweb.vehiclerouting.service.location; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.util.Optional; import javax.inject.Inject; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectMock; @QuarkusTest class LocationServiceIntegrationTest { @InjectMock DistanceMatrix distanceMatrix; @Inject LocationService locationService; @Test void location_service_should_be_transactional() { when(distanceMatrix.addLocation(any())).thenReturn(locationId -> Distance.ZERO); when(distanceMatrix.distance(any(), any())).thenReturn(Distance.ZERO); locationService.addLocation(new Location(1000, Coordinates.of(-1, 12))); locationService.createLocation(Coordinates.of(12, -1), "location 1"); Optional location = locationService.createLocation(Coordinates.of(32, -5), "location 2"); assertThat(location).isNotEmpty(); locationService.populateDistanceMatrix(); locationService.removeLocation(location.get().id()); locationService.removeAll(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/location/LocationServiceTest.java ================================================ package org.optaweb.vehiclerouting.service.location; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.Collections; import java.util.Optional; import javax.enterprise.event.Event; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.service.distance.DistanceRepository; import org.optaweb.vehiclerouting.service.error.ErrorEvent; @ExtendWith(MockitoExtension.class) class LocationServiceTest { @Mock private LocationRepository repository; @Mock private DistanceRepository distanceRepository; @Mock private LocationPlanner planner; @Mock private DistanceMatrix distanceMatrix; @Mock private Event errorEvent; @InjectMocks private LocationService locationService; private final Coordinates coordinates = Coordinates.of(0.0, 1.0); private final Location location = new Location(1, coordinates); @Test void createLocation_should_validate_arguments() { assertThatNullPointerException().isThrownBy(() -> locationService.createLocation(null, "x")); assertThatNullPointerException().isThrownBy(() -> locationService.createLocation(coordinates, null)); } @Test void createLocation(@Mock DistanceMatrixRow matrixRow) { Distance distance = Distance.ofMillis(123); Location existingLocation = new Location(2, coordinates); when(repository.locations()).thenReturn(Arrays.asList(existingLocation)); String description = "new location"; when(repository.createLocation(coordinates, description)).thenReturn(location); when(distanceMatrix.addLocation(any())).thenReturn(matrixRow); when(distanceMatrix.distance(any(), any())).thenReturn(distance); when(matrixRow.distanceTo(anyLong())).thenReturn(distance); assertThat(locationService.createLocation(coordinates, description)).contains(location); verify(repository).createLocation(coordinates, description); verify(distanceMatrix).addLocation(location); verify(distanceRepository).saveDistance(existingLocation, location, distance); verify(distanceRepository).saveDistance(location, existingLocation, distance); verify(planner).addLocation(location, matrixRow); } @Test void addLocation_should_validate_arguments() { assertThatNullPointerException().isThrownBy(() -> locationService.addLocation(null)); } @Test void addLocation(@Mock DistanceMatrixRow matrixRow) { when(distanceMatrix.addLocation(any())).thenReturn(matrixRow); locationService.addLocation(location); verifyNoInteractions(repository); verifyNoInteractions(distanceRepository); verify(distanceMatrix).addLocation(location); verify(planner).addLocation(location, matrixRow); } @Test void removing_depot_should_be_successful_when_it_is_the_last_location() { when(repository.locations()).thenReturn(Collections.singletonList(location)); when(repository.find(location.id())).thenReturn(Optional.of(location)); locationService.removeLocation(location.id()); verify(repository).removeLocation(location.id()); verify(distanceRepository).deleteDistances(location); verify(planner).removeLocation(location); verifyNoInteractions(errorEvent); // TODO remove location from distance matrix } @Test void removing_nonexistent_location_should_publish_error() { when(repository.find(location.id())).thenReturn(Optional.empty()); locationService.removeLocation(location.id()); verifyNoInteractions(planner); verify(repository, never()).removeLocation(anyLong()); verify(distanceRepository, never()).deleteDistances(any(Location.class)); verify(errorEvent).fire(any(ErrorEvent.class)); } @Test void removing_depot_when_there_are_other_locations_should_publish_error() { Location depot = new Location(1, coordinates); Location visit = new Location(2, coordinates); when(repository.locations()).thenReturn(Arrays.asList(depot, visit)); when(repository.find(depot.id())).thenReturn(Optional.of(depot)); locationService.removeLocation(depot.id()); verifyNoInteractions(planner); verifyNoInteractions(distanceMatrix); verify(repository, never()).removeLocation(anyLong()); verify(distanceRepository, never()).deleteDistances(any(Location.class)); verify(errorEvent).fire(any(ErrorEvent.class)); } @Test void removing_visit_should_be_successful() { Location depot = new Location(1, coordinates); Location visit = new Location(2, coordinates); when(repository.locations()).thenReturn(Arrays.asList(depot, visit)); when(repository.find(visit.id())).thenReturn(Optional.of(visit)); locationService.removeLocation(visit.id()); verify(planner).removeLocation(visit); verify(distanceMatrix).removeLocation(visit); verify(repository).removeLocation(visit.id()); verify(distanceRepository).deleteDistances(visit); verifyNoInteractions(errorEvent); } @Test void clear() { locationService.removeAll(); verify(planner).removeAllLocations(); verify(repository).removeAll(); verify(distanceRepository).deleteAll(); verify(distanceMatrix).clear(); } @Test void should_not_optimize_and_roll_back_if_distance_calculation_fails() { when(repository.createLocation(coordinates, "")).thenReturn(location); doThrow(new RuntimeException("test exception")).when(distanceMatrix).addLocation(location); assertThat(locationService.createLocation(coordinates, "")).isEmpty(); verifyNoInteractions(planner); verifyNoInteractions(distanceRepository); // publish error event verify(errorEvent).fire(any(ErrorEvent.class)); // roll back verify(repository).removeLocation(location.id()); } @Test void populate_matrix_should_read_all_distances() { Location depot = new Location(1, coordinates); Location visit1 = new Location(2, coordinates); Location visit2 = new Location(3, coordinates); when(repository.locations()).thenReturn(Arrays.asList(depot, visit1, visit2)); when(distanceRepository.getDistance(any(Location.class), any(Location.class))).thenReturn(Optional.of(Distance.ZERO)); locationService.populateDistanceMatrix(); verify(distanceRepository, times(6)).getDistance(any(Location.class), any(Location.class)); verify(distanceMatrix, times(6)).put(any(Location.class), any(Location.class), any(Distance.class)); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/region/BoundingBoxTest.java ================================================ package org.optaweb.vehiclerouting.service.region; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Coordinates; class BoundingBoxTest { @Test void validate_southwest_and_northeast_arguments() { // 1───┐ // │ ↘ │ // └───2 assertThatIllegalArgumentException().isThrownBy(() -> new BoundingBox( Coordinates.of(9.9, -1.0), // NW Coordinates.of(1.0, 1.01) // SE )).withMessageMatching(".*\\(9\\.9N.*\\(1\\.0N.*"); // 2───┐ // │ ↖ │ // └───1 assertThatIllegalArgumentException().isThrownBy(() -> new BoundingBox( Coordinates.of(-1.0, 9.9), // SE Coordinates.of(1.01, 1.0) // NW )).withMessageMatching(".*\\(9\\.9E.*\\(1\\.0E.*"); // ┌───1 // │ ↙ │ // 2───┘ assertThatIllegalArgumentException().isThrownBy(() -> new BoundingBox( Coordinates.of(9.9, 9.9), // NE Coordinates.of(1.0, 1.0) // SW )).withMessageMatching(".*\\(9\\.9N.*\\(1\\.0N.*"); } @Test void should_fail_if_bounding_box_has_zero_dimension() { // // ╶───╴ // assertThatIllegalArgumentException().isThrownBy(() -> new BoundingBox( Coordinates.of(0.0, 1.0), Coordinates.of(0.0, 2.0))).withMessageMatching(".*\\(0\\.0N.*\\(0\\.0N.*"); // ╷ // │ // ╵ assertThatIllegalArgumentException().isThrownBy(() -> new BoundingBox( Coordinates.of(0.0, 10.0), Coordinates.of(1.0, 10.0))).withMessageMatching(".*\\(10\\.0E.*\\(10\\.0E.*"); } @Test void constructor_args_not_null() { assertThatNullPointerException().isThrownBy(() -> new BoundingBox(null, Coordinates.of(1.0, 1.0))); assertThatNullPointerException().isThrownBy(() -> new BoundingBox(Coordinates.of(1.0, 1.0), null)); } @Test void should_work_with_southwest_and_northeast() { // ┌───2 // │ ↗ │ // 1───┘ Coordinates sw = Coordinates.of(-10.0, -100.0); Coordinates ne = Coordinates.of(20.0, -2.0); BoundingBox boundingBox = new BoundingBox(sw, ne); assertThat(boundingBox.getSouthWest()).isEqualTo(sw); assertThat(boundingBox.getNorthEast()).isEqualTo(ne); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/region/RegionPropertiesTest.java ================================================ package org.optaweb.vehiclerouting.service.region; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import javax.inject.Inject; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest class RegionPropertiesTest { @Inject RegionProperties regionProperties; @Test void test() { assertThat(regionProperties.countryCodes()).contains(List.of("AT", "DE")); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/region/RegionServiceTest.java ================================================ package org.optaweb.vehiclerouting.service.region; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; @ExtendWith(MockitoExtension.class) class RegionServiceTest { @Mock private RegionProperties regionProperties; @Mock private Region region; @InjectMocks private RegionService regionService; @Test void should_return_country_codes_from_properties() { List countryCodes = Arrays.asList("XY", "WZ"); when(regionProperties.countryCodes()).thenReturn(Optional.of(countryCodes)); assertThat(regionService.countryCodes()).isEqualTo(countryCodes); } @Test void should_return_graphHopper_bounds() { BoundingBox boundingBox = new BoundingBox(Coordinates.of(-1, -2), Coordinates.of(3, 4)); when(region.getBounds()).thenReturn(boundingBox); assertThat(regionService.boundingBox()).isEqualTo(boundingBox); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/reload/ReloadServiceTest.java ================================================ package org.optaweb.vehiclerouting.service.reload; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleFactory; import org.optaweb.vehiclerouting.service.location.LocationRepository; import org.optaweb.vehiclerouting.service.location.LocationService; import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository; import org.optaweb.vehiclerouting.service.vehicle.VehicleService; import io.quarkus.runtime.StartupEvent; @ExtendWith(MockitoExtension.class) class ReloadServiceTest { @Mock private VehicleRepository vehicleRepository; @Mock private VehicleService vehicleService; @Mock private LocationRepository locationRepository; @Mock private LocationService locationService; @InjectMocks private ReloadService reloadService; @Mock StartupEvent event; private final Vehicle vehicle = VehicleFactory.createVehicle(193, "Vehicle 193", 100); private final List persistedVehicles = Arrays.asList(vehicle, vehicle); private final Location location = new Location(1, Coordinates.of(0.0, 1.0)); private final List persistedLocations = Arrays.asList(location, location, location); @Test void should_reload_on_startup() { when(vehicleRepository.vehicles()).thenReturn(persistedVehicles); when(locationRepository.locations()).thenReturn(persistedLocations); reloadService.reload(event); verify(vehicleRepository).vehicles(); verify(vehicleService, times(persistedVehicles.size())).addVehicle(vehicle); verify(locationRepository).locations(); verify(locationService, times(persistedLocations.size())).addLocation(location); verify(locationService).populateDistanceMatrix(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/route/RouteListenerTest.java ================================================ package org.optaweb.vehiclerouting.service.route; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.List; import java.util.Optional; import javax.enterprise.event.Event; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Coordinates; import org.optaweb.vehiclerouting.domain.Distance; import org.optaweb.vehiclerouting.domain.Location; import org.optaweb.vehiclerouting.domain.RouteWithTrack; import org.optaweb.vehiclerouting.domain.RoutingPlan; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleFactory; import org.optaweb.vehiclerouting.service.location.LocationRepository; import org.optaweb.vehiclerouting.service.vehicle.VehicleRepository; @ExtendWith(MockitoExtension.class) class RouteListenerTest { @Mock private Router router; @Mock private VehicleRepository vehicleRepository; @Mock private LocationRepository locationRepository; @Mock private Event routingPlanEvent; @Captor private ArgumentCaptor routeArgumentCaptor; @InjectMocks private RouteListener routeListener; @Test void new_listener_should_return_empty_best_route() { assertThat(routeListener.getBestRoutingPlan().isEmpty()).isTrue(); } @Test void event_with_no_routes_should_be_consumed_as_an_empty_routing_plan() { final long vehicleId = 12; final Vehicle vehicle = VehicleFactory.testVehicle(vehicleId); when(vehicleRepository.find(vehicleId)).thenReturn(Optional.of(vehicle)); RouteChangedEvent event = new RouteChangedEvent( this, Distance.ZERO, singletonList(vehicleId), null, emptyList(), emptyList()); routeListener.onApplicationEvent(event); verifyNoInteractions(router); RoutingPlan routingPlan = verifyAndCaptureConsumedPlan(); assertThat(routingPlan.vehicles()).containsExactly(vehicle); assertThat(routingPlan.depot()).isEmpty(); assertThat(routingPlan.visits()).isEmpty(); assertThat(routingPlan.routes()).isEmpty(); } @Test void event_with_no_visits_and_a_depot_should_be_consumed_as_plan_with_empty_routes() { final Coordinates depotCoordinates = Coordinates.of(0.0, 0.1); final Location depot = new Location(1, depotCoordinates); final long vehicleId = 448; final Vehicle vehicle = VehicleFactory.testVehicle(vehicleId); ShallowRoute route = new ShallowRoute(vehicle.id(), depot.id(), emptyList()); when(vehicleRepository.find(vehicleId)).thenReturn(Optional.of(vehicle)); when(locationRepository.find(depot.id())).thenReturn(Optional.of(depot)); RouteChangedEvent event = new RouteChangedEvent( this, Distance.ofMillis(5000), singletonList(vehicleId), depot.id(), emptyList(), singletonList(route)); routeListener.onApplicationEvent(event); verifyNoInteractions(router); RoutingPlan routingPlan = verifyAndCaptureConsumedPlan(); assertThat(routingPlan.vehicles()).containsExactly(vehicle); assertThat(routingPlan.depot()).contains(depot); assertThat(routingPlan.visits()).isEmpty(); assertThat(routingPlan.routes()).hasSize(1); RouteWithTrack routeWithTrack = routingPlan.routes().iterator().next(); assertThat(routeWithTrack.depot()).isEqualTo(depot); assertThat(routeWithTrack.vehicle()).isEqualTo(vehicle); assertThat(routeWithTrack.visits()).isEmpty(); assertThat(routeWithTrack.track()).isEmpty(); } @Test void listener_should_pass_routing_plan_to_consumer_when_an_update_event_occurs() { final Coordinates depotCoordinates = Coordinates.of(0.0, 0.1); final Coordinates visitCoordinates = Coordinates.of(2.0, -0.2); final Coordinates checkpoint1 = Coordinates.of(12, 12); final Coordinates checkpoint2 = Coordinates.of(21, 21); List path1 = Arrays.asList(depotCoordinates, checkpoint1, checkpoint2, visitCoordinates); List path2 = Arrays.asList(visitCoordinates, checkpoint2, checkpoint1, depotCoordinates); when(router.getPath(depotCoordinates, visitCoordinates)).thenReturn(path1); when(router.getPath(visitCoordinates, depotCoordinates)).thenReturn(path2); final long vehicleId = -5; final Vehicle vehicle = VehicleFactory.testVehicle(vehicleId); final Location depot = new Location(1, depotCoordinates); final Location visit = new Location(2, visitCoordinates); final Distance distance = Distance.ofMillis(11); when(vehicleRepository.find(vehicleId)).thenReturn(Optional.of(vehicle)); when(locationRepository.find(depot.id())).thenReturn(Optional.of(depot)); when(locationRepository.find(visit.id())).thenReturn(Optional.of(visit)); ShallowRoute route = new ShallowRoute(vehicle.id(), depot.id(), singletonList(visit.id())); RouteChangedEvent event = new RouteChangedEvent( this, distance, singletonList(vehicleId), depot.id(), singletonList(visit.id()), singletonList(route)); routeListener.onApplicationEvent(event); RoutingPlan routingPlan = verifyAndCaptureConsumedPlan(); assertThat(routingPlan.distance()).isEqualTo(distance); assertThat(routingPlan.vehicles()).containsExactly(vehicle); assertThat(routingPlan.depot()).contains(depot); assertThat(routingPlan.visits()).containsExactly(visit); assertThat(routingPlan.routes()).hasSize(1); RouteWithTrack routeWithTrack = routingPlan.routes().iterator().next(); assertThat(routeWithTrack.vehicle()).isEqualTo(vehicle); assertThat(routeWithTrack.depot()).isEqualTo(depot); assertThat(routeWithTrack.visits()).containsExactly(visit); assertThat(routeWithTrack.track()).containsExactly(path1, path2); assertThat(routeListener.getBestRoutingPlan()).isEqualTo(routingPlan); } @Test void should_discard_update_gracefully_if_one_of_the_locations_no_longer_exist() { final Vehicle vehicle = VehicleFactory.testVehicle(3); final Location depot = new Location(1, Coordinates.of(1.0, 2.0)); final Location visit = new Location(2, Coordinates.of(-1.0, -2.0)); when(vehicleRepository.find(vehicle.id())).thenReturn(Optional.of(vehicle)); when(locationRepository.find(depot.id())).thenReturn(Optional.of(depot)); when(locationRepository.find(visit.id())).thenReturn(Optional.empty()); ShallowRoute route = new ShallowRoute(vehicle.id(), depot.id(), singletonList(visit.id())); RouteChangedEvent event = new RouteChangedEvent( this, Distance.ofMillis(1), singletonList(vehicle.id()), depot.id(), singletonList(visit.id()), singletonList(route)); // precondition assertThat(routeListener.getBestRoutingPlan().isEmpty()).isTrue(); // must not throw exception routeListener.onApplicationEvent(event); verify(router, never()).getPath(any(), any()); verify(routingPlanEvent, never()).fire(any()); assertThat(routeListener.getBestRoutingPlan().isEmpty()).isTrue(); } @Test void should_discard_update_gracefully_if_one_of_the_vehicles_no_longer_exist() { final Vehicle vehicle = VehicleFactory.testVehicle(3); final Location depot = new Location(1, Coordinates.of(1.0, 2.0)); final Location visit = new Location(2, Coordinates.of(-1.0, -2.0)); when(vehicleRepository.find(vehicle.id())).thenReturn(Optional.empty()); when(locationRepository.find(depot.id())).thenReturn(Optional.of(depot)); ShallowRoute route = new ShallowRoute(vehicle.id(), depot.id(), singletonList(visit.id())); RouteChangedEvent event = new RouteChangedEvent( this, Distance.ofMillis(1), singletonList(vehicle.id()), depot.id(), singletonList(visit.id()), singletonList(route)); // precondition assertThat(routeListener.getBestRoutingPlan().isEmpty()).isTrue(); // must not throw exception routeListener.onApplicationEvent(event); verify(router, never()).getPath(any(), any()); verify(routingPlanEvent, never()).fire(any()); assertThat(routeListener.getBestRoutingPlan().isEmpty()).isTrue(); } private RoutingPlan verifyAndCaptureConsumedPlan() { verify(routingPlanEvent).fire(routeArgumentCaptor.capture()); return routeArgumentCaptor.getValue(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/route/ShallowRouteTest.java ================================================ package org.optaweb.vehiclerouting.service.route; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import org.junit.jupiter.api.Test; class ShallowRouteTest { @Test void shallow_route_to_string() { ShallowRoute shallowRoute = new ShallowRoute(200L, 100L, Arrays.asList(93L, 92L, 91L)); assertThat(shallowRoute.toString()).containsSubsequence("200", "100", "93", "92", "91"); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/vehicle/VehicleServiceIntegrationTest.java ================================================ package org.optaweb.vehiclerouting.service.vehicle; import javax.inject.Inject; import org.junit.jupiter.api.Test; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleFactory; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest class VehicleServiceIntegrationTest { @Inject VehicleService vehicleService; @Test void vehicle_service_should_be_transactional() { vehicleService.addVehicle(VehicleFactory.testVehicle(1000)); Vehicle vehicle1 = vehicleService.createVehicle(VehicleFactory.vehicleData("vehicle", 1)); Vehicle vehicle2 = vehicleService.createVehicle(); vehicleService.changeCapacity(vehicle2.id(), vehicle2.capacity() + 100); vehicleService.createVehicle(); vehicleService.removeVehicle(vehicle1.id()); vehicleService.removeAnyVehicle(); vehicleService.removeAll(); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/service/vehicle/VehicleServiceTest.java ================================================ package org.optaweb.vehiclerouting.service.vehicle; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.optaweb.vehiclerouting.domain.Vehicle; import org.optaweb.vehiclerouting.domain.VehicleData; import org.optaweb.vehiclerouting.domain.VehicleFactory; @ExtendWith(MockitoExtension.class) class VehicleServiceTest { @Captor private ArgumentCaptor vehicleArgumentCaptor; @Mock private VehiclePlanner planner; @Mock private VehicleRepository vehicleRepository; @InjectMocks private VehicleService vehicleService; @Test void create_default_vehicle() { final long vehicleId = 63; final String name = "Veh5"; final int capacity = VehicleService.DEFAULT_VEHICLE_CAPACITY * 2 + 29; final Vehicle vehicle = VehicleFactory.createVehicle(vehicleId, name, capacity); // verify that new vehicle is created with correct initial name and capacity when(vehicleRepository.createVehicle(VehicleService.DEFAULT_VEHICLE_CAPACITY)).thenReturn(vehicle); assertThat(vehicleService.createVehicle()).isEqualTo(vehicle); // verify that vehicle provided by repository is passed to planner verify(planner).addVehicle(vehicleArgumentCaptor.capture()); Vehicle newVehicle = vehicleArgumentCaptor.getValue(); assertThat(newVehicle.id()).isEqualTo(vehicleId); assertThat(newVehicle.name()).isEqualTo(name); assertThat(newVehicle.capacity()).isEqualTo(capacity); } @Test void createVehicle() { final long vehicleId = 63; final String name = "Veh5"; final int capacity = 101; VehicleData vehicleData = VehicleFactory.vehicleData(name, capacity); final Vehicle vehicle = VehicleFactory.createVehicle(vehicleId, name, capacity); when(vehicleRepository.createVehicle(vehicleData)).thenReturn(vehicle); assertThat(vehicleService.createVehicle(vehicleData)).isEqualTo(vehicle); // verify that vehicle provided by repository is passed to planner verify(planner).addVehicle(vehicle); } @Test void addVehicle_should_validate_arguments() { assertThatNullPointerException().isThrownBy(() -> vehicleService.addVehicle(null)); } @Test void addVehicle() { final Vehicle vehicle = VehicleFactory.testVehicle(1); vehicleService.addVehicle(vehicle); verifyNoInteractions(vehicleRepository); verify(planner).addVehicle(vehicle); } @Test void removeVehicle() { final long vehicleId = 8; final Vehicle vehicle = VehicleFactory.testVehicle(vehicleId); when(vehicleRepository.removeVehicle(vehicleId)).thenReturn(vehicle); vehicleService.removeVehicle(vehicleId); verify(vehicleRepository).removeVehicle(vehicleId); verify(planner).removeVehicle(vehicle); } @Test void removeAnyVehicle_should_remove_oldest_vehicle() { final long vehicleId1 = 1; final long vehicleId2 = 2; final long vehicleId3 = 3; final Vehicle vehicle1 = VehicleFactory.testVehicle(vehicleId1); final Vehicle vehicle2 = VehicleFactory.testVehicle(vehicleId2); final Vehicle vehicle3 = VehicleFactory.testVehicle(vehicleId3); when(vehicleRepository.vehicles()).thenReturn(asList(vehicle3, vehicle1, vehicle2)); when(vehicleRepository.removeVehicle(vehicleId1)).thenReturn(vehicle1); vehicleService.removeAnyVehicle(); verify(vehicleRepository).removeVehicle(vehicleId1); verify(planner).removeVehicle(vehicle1); } @Test void removeAll() { vehicleService.removeAll(); verify(planner).removeAllVehicles(); verify(vehicleRepository).removeAll(); } @Test void changeCapacity() { final long vehicleId = 1; final int capacity = 123; final Vehicle vehicle = VehicleFactory.createVehicle(vehicleId, "1", capacity); when(vehicleRepository.changeCapacity(vehicleId, capacity)).thenReturn(vehicle); vehicleService.changeCapacity(vehicleId, capacity); verify(vehicleRepository).changeCapacity(vehicleId, capacity); verify(planner).changeCapacity(vehicleArgumentCaptor.capture()); assertThat(vehicleArgumentCaptor.getValue().capacity()).isEqualTo(capacity); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/util/jackson/JacksonAssertions.java ================================================ package org.optaweb.vehiclerouting.util.jackson; public class JacksonAssertions { public static ObjectAssert assertThat(T actual) { return new ObjectAssert<>(actual); } public static JsonAssert assertThat(String actual) { return new JsonAssert(actual); } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/util/jackson/JsonAssert.java ================================================ package org.optaweb.vehiclerouting.util.jackson; import org.assertj.core.api.Assertions; import org.assertj.core.api.StringAssert; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; public class JsonAssert extends StringAssert { private final ObjectMapper objectMapper = new ObjectMapper(); protected JsonAssert(String actual) { super(actual); } public void deserializedIsEqualTo(Object expected) { isNotNull(); try { Object value = objectMapper.readValue(actual, expected.getClass()); Assertions.assertThat(value).isEqualTo(expected); } catch (JsonProcessingException e) { throw new AssertionError("ObjectMapper.readValue(actual, expected.getClass()) called with actual: <" + actual + "> and expected: <" + e + "> threw an exception.", e); } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/util/jackson/ObjectAssert.java ================================================ package org.optaweb.vehiclerouting.util.jackson; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; public class ObjectAssert extends AbstractAssert, T> { private final ObjectMapper objectMapper = new ObjectMapper(); protected ObjectAssert(T actual) { super(actual, ObjectAssert.class); } public void serializedIsEqualToJson(String expected) { isNotNull(); try { String actualSerialized = objectMapper.writeValueAsString(actual); Assertions.assertThat(actualSerialized).isEqualToIgnoringWhitespace(expected); } catch (JsonProcessingException e) { throw new AssertionError("ObjectMapper.writeValueAsString(actual) called with actual: <" + actual + "> threw an exception.", e); } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/util/junit/FileContent.java ================================================ package org.optaweb.vehiclerouting.util.junit; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.extension.ExtendWith; @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(FileContentExtension.class) public @interface FileContent { String value(); } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/java/org/optaweb/vehiclerouting/util/junit/FileContentExtension.java ================================================ package org.optaweb.vehiclerouting.util.junit; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; public class FileContentExtension implements ParameterResolver { @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return parameterContext.isAnnotated(FileContent.class) && parameterContext.getParameter().getType().equals(String.class); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { String resourceName = parameterContext.findAnnotation(FileContent.class).orElseThrow().value(); URL resource = parameterContext.getTarget().orElseThrow().getClass().getResource(resourceName); if (resource == null) { throw new AssertionError("Resource <" + resourceName + "> cannot be loaded."); } try { return Files.readString(Path.of(resource.toURI())); } catch (URISyntaxException | IOException e) { throw new AssertionError("Failed to read resource <" + resourceName + ">.", e); } } } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/resources/mockito-extensions/README.adoc ================================================ The Mockito extension file link:org.mockito.plugins.MockMaker[org.mockito.plugins.MockMaker] enables https://javadoc.io/page/org.mockito/mockito-core/latest/org/mockito/Mockito.html#39[ Mocking final types]. Needed to mock `com.graphhopper.storage.GraphHopperStorage`. ================================================ FILE: optaweb-vehicle-routing-backend/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline ================================================ FILE: optaweb-vehicle-routing-backend/src/test/resources/org/optaweb/vehiclerouting/plugin/rest/model/portable-error-message.json ================================================ { "id": "c670dd37-62fb-4e86-95ed-c1f4953aaeaa", "text": "Error message.\nDetails." } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/resources/org/optaweb/vehiclerouting/plugin/rest/model/portable-location.json ================================================ { "id": 987, "lat": 1, "lng": 10, "description": "Some Location" } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/resources/org/optaweb/vehiclerouting/plugin/rest/model/portable-route.json ================================================ { "vehicle": {"id": 13, "name": "Vehicle", "capacity": 45317}, "depot": {"id": 8, "lat": 42.6501218, "lng": -71.8835449, "description": "Test depot"}, "visits": [ {"id": 100, "lat": 42.7066596, "lng": -72.4934873, "description": "Visit 1"}, {"id": 200, "lat": 42.5543343, "lng": -71.443828, "description": "Visit 2"} ], "track": [[[42.65005, -71.88522], [42.64997, -71.88527]], [[42.64994, -71.88537], [42.64994, -71.88542]]] } ================================================ FILE: optaweb-vehicle-routing-backend/src/test/resources/org/optaweb/vehiclerouting/plugin/routing/CREDITS.adoc ================================================ All `.osm.pbf` files in this folder contain data link:https://www.openstreetmap.org/copyright[copyrighted by OpenStreetMap contributors] and licensed under link:https://opendatacommons.org/licenses/odbl/[Open Database License (ODbL)]. ================================================ FILE: optaweb-vehicle-routing-backend/src/test/resources/org/optaweb/vehiclerouting/service/demo/dataset/test-belgium.yaml ================================================ --- name: "Belgium test" depot: label: "Brussels" lat: 50.85 lng: 4.35 visits: - label: "Aalst" lat: 50.933333 lng: 4.033333 - label: "Châtelet" lat: 50.4 lng: 4.516667 - label: "La Louvière" lat: 50.466667 lng: 4.183333 - label: "Sint-Niklaas" lat: 51.166667 lng: 4.133333 # random order - lng: 2.883333 lat: 50.85 label: "Ypres" vehicles: - name: "vehicle 1" capacity: 10 - name: "vehicle 2" capacity: 12 - name: "vehicle 3" capacity: 1000000 ================================================ FILE: optaweb-vehicle-routing-distribution/.gitignore ================================================ /target /local ================================================ FILE: optaweb-vehicle-routing-distribution/pom.xml ================================================ 4.0.0 org.optaweb.vehiclerouting optaweb-vehicle-routing 8.35.0.Final optaweb-vehicle-routing-distribution pom OptaWeb Vehicle Routing Distribution org.optaweb.vehiclerouting optaweb-vehicle-routing-docs zip org.optaweb.vehiclerouting optaweb-vehicle-routing-standalone quarkus-app zip org.apache.maven.plugins maven-assembly-plugin package single src/main/assembly/assembly-optaweb-vehicle-routing.xml false ================================================ FILE: optaweb-vehicle-routing-distribution/src/main/assembly/assembly-optaweb-vehicle-routing.xml ================================================ assembly-optaweb-vehicle-routing zip true ../LICENSE.txt src/main/resources/README.adoc ../runLocally.sh bin true false .. sources **/target/** **/local/** **/.profiler/** **/.project **/.idea/** **/*.ipr **/*.iws **/*.iml **/nbproject/** **/.vscode/** **/.DS_Store .git/** optaweb-vehicle-routing-frontend/.scannerwork/** optaweb-vehicle-routing-frontend/build/** optaweb-vehicle-routing-frontend/coverage/** optaweb-vehicle-routing-frontend/cypress/screenshots/** optaweb-vehicle-routing-frontend/cypress/videos/** optaweb-vehicle-routing-frontend/docker/build/** optaweb-vehicle-routing-frontend/node/** optaweb-vehicle-routing-frontend/node_modules/** upstream-repos/** org.optaweb.vehiclerouting:optaweb-vehicle-routing-standalone:zip:quarkus-app bin true true false org.optaweb.vehiclerouting:optaweb-vehicle-routing-docs:zip reference_manual true false true ================================================ FILE: optaweb-vehicle-routing-distribution/src/main/resources/README.adoc ================================================ = Welcome to OptaWeb Vehicle Routing == Quick start If you want to run the application without building it from source, go to `bin` directory and use the run script. You only need Java Runtime Environment (JRE). == Documentation Read the complete documentation under `reference_manual` directory in HTML or PDF format. == Build from source The distribution contains complete project sources under the `sources` directory. You can build the project with Maven. If you want to start hacking on the distributed sources, it might be a good idea to initialize a Git repository first so that you can keep track of your changes. You can do that with ---- cd sources/ git init && git add . && git commit -m 'Initial commit' ---- Find out more in `sources/README.adoc` or in the documentation. == Run on OpenShift Build the project first. Then deploy it to OpenShift using the `runOnOpenShift.sh` script under `sources` directory. == Get help In case you need any kind of help, please go to https://www.optaplanner.org/community/getHelp.html. You will find a way to contact us whether you need to ask a question, report a bug or start a discussion. ================================================ FILE: optaweb-vehicle-routing-docs/.gitignore ================================================ /target /local ================================================ FILE: optaweb-vehicle-routing-docs/pom.xml ================================================ 4.0.0 org.optaweb.vehiclerouting optaweb-vehicle-routing 8.35.0.Final optaweb-vehicle-routing-docs pom OptaWeb Vehicle Routing Documentation org.asciidoctor asciidoctor-maven-plugin index.adoc ${project.version} . generate-single-html generate-resources process-asciidoc html5 highlightjs ${project.build.directory}/generated-docs/html_single generate-pdf generate-resources process-asciidoc pdf coderay ${project.build.directory}/generated-docs/pdf ${project.artifactId}.pdf org.apache.maven.plugins maven-assembly-plugin package-generated-docs package single ${project.artifactId}-${project.version} false src/main/assembly/assembly-generated-docs-zip.xml true htmlOnly true org.asciidoctor asciidoctor-maven-plugin generate-pdf none ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/appendix-backend-architecture.adoc ================================================ [appendix] [[backend-architecture]] = Back end architecture Domain model and use cases are essential for the application. We put domain model at the center of the architecture and surround it by the application layer that embeds use cases. Functions such as route optimization, distance calculation, persistence, and network communication are considered implementation details and are placed at the outermost layer of the architecture. .Diagram of application layers image::backend-layers.svg[align="center"] == Code organization The back end code is organized in three layers outlined above. [literal] .... org.optaweb.vehiclerouting ├── domain ├── plugin # Infrastructure layer │   ├── persistence │   ├── planner │   ├── routing │   └── websocket └── service # Application layer ├── demo ├── distance ├── location ├── region ├── reload ├── route └── vehicle .... The `service` package contains the application layer that implements use cases. The `plugin` package contains the infrastructure layer. Code in each layer is further organized by function. This means that each service or plugin has its own package. == Dependency rules Compile-time dependencies are only allowed to point from outer layers towards the center. Following this rule helps to keep the domain model independent of underlying frameworks and other implementation details and model the behavior of business entities more precisely. With presentation and persistence being pushed out to the periphery, it is easier to test the behavior of business entities and use cases. The domain has no dependencies. Services only depend on the domain. If a service needs to send a result (for example to the database or to the client), it uses an output boundary interface. Its implementation is injected by the https://quarkus.io/guides/cdi[CDI container]. Plugins depend on services in two ways. Firstly, they invoke services based on events such as a user input or a route update coming from the optimization engine. Services are injected into plugins which moves the burden of their construction and dependency resolution to the IoC container. Secondly, plugins implement service output boundary interfaces to handle use case results, for example persisting changes to the database or sending a response to the web UI. == The `domain` package The `domain` package contains _business objects_ that model the domain of this project, for example `Location`, `Vehicle`, `Route`. These objects are strictly business-oriented and must not be influenced by any tools and frameworks, for example object-relational mapping tools and web service frameworks. == The `service` package The `service` package contains classes that implement _use cases_. A use case describes something that you want to do, for example adding new location, changing vehicle capacity, or finding coordinates for an address. The business rules that govern use cases are expressed using the domain objects. Services often need to interact with plugins in the outer layer, such as persistence, web, and optimization. To satisfy the dependency rules between layers, the interaction between services and plugins is expressed in terms of interfaces that define the dependencies of a service. A plugin can satisfy a dependency of a service by providing a bean that implements the service's boundary interface. The CDI container creates an instance of the plugin bean and injects it to the service at runtime. This is an example of the inversion of control principle. == The `plugin` package The `plugin` package contains infrastructure functions such as optimization, persistence, routing, and network. ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/appendix-backend-config.adoc ================================================ [appendix] [[backend-configuration-properties]] = Back end configuration properties [cols="m,d,a,d",options="header"] |=== |Property |Type |Example |Description |app.demo.data-set-dir |Relative or absolute path |/home/user/{data-dir-name}/dataset |Custom <> are loaded from this directory. Defaults to `local/dataset`. |app.persistence.h2-dir |Relative or absolute path |/home/user/{data-dir-name}/db |The directory used by H2 to store the database file. Defaults to `local/db`. |app.region.country-codes |List of https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2[ISO 3166-1 alpha-2] country codes |`US`, `GB,IE`, `DE,AT,CH`, may be empty |Restricts geosearch results. |app.routing.engine |Enumeration |`air`, `graphhopper` |Routing engine implementation. Defaults to `graphhopper`. |app.routing.gh-dir |Relative or absolute path |/home/user/{data-dir-name}/graphhopper |The directory used by GraphHopper to store road network graphs. Defaults to `local/graphhopper`. |app.routing.osm-dir |Relative or absolute path |/home/user/{data-dir-name}/openstreetmap |The directory that contains OSM files. Defaults to `local/openstreetmap`. |app.routing.osm-file |File name |belgium-latest.osm.pbf |Name of the OSM file that should be loaded by GraphHopper. The file must be placed under `app.routing.osm-dir`. |optaplanner.solver.termination.spent-limit |java.time.Duration |* 1m * 150s * P2dT21h (PnDTnHnMn.nS) |How long the solver should run after a location change occurs. |server.address |IP address or hostname |10.0.0.123, my-vrp.geo-1.openshiftapps.com |Network address to which the server should bind. |server.port |Port number |4000, 8081 |Server HTTP port. |=== ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/attributes.adoc ================================================ :data-dir-name: .optaweb-vehicle-routing ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/contributing.adoc ================================================ [[contributing]] = Contributing to OptaWeb Vehicle Routing == Formatting OptaWeb Vehicle Routing source code using Maven The OptaWeb Vehicle Routing back end has a strictly enforced code style. Code formatting is performed by the Eclipse code formatter, using configuration files from the OptaPlanner project. By default, when you execute the `./mvnw install` command, the code is formatted automatically. This is important because when you submit a pull request, the continuous integration (CI) build fails if running the formatter results in any code changes. Therefore, it is recommended that you always run a full Maven build before submitting a pull request. .Procedure . To run a full Maven build, enter the following command: + [source] ---- ./mvnw install ---- + . Optional: To run the formatter without performing a full build, enter the following command: + [source] ---- ./mvnw process-sources ---- == Formatting OptaWeb Vehicle Routing source code using an IDE You can use Eclipse IDE or IntelliJ IDEA to format the source code without having to invoke the Maven build. This allows you to make small changes and commit often while making sure that the source code is formatted properly and the Maven build is successful after each commit. You must configure your IDE to use the Eclipse formatter plugin and select the formatter configuration used by the Maven plugin that formats the source code automatically during the Maven build. The formatter configuration files are stored in the `build/optaplanner-ide-config/src/main/resources` directory under the root of the OptaPlanner repository. === Eclipse IDE setup .Prerequisites * The OptaPlanner repository is cloned on your computer. Complete the following steps to configure Eclipse IDE: .Procedure . Open menu:Window[Preferences] and then navigate to menu:Java[Code Style > Formatter]. . Click *Import...* and select `optaplanner/build/optaplanner-ide-config/src/main/resources/eclipse-format.xml`. . Navigate to menu:Java[Code Style > Organize Imports]. . Click *Import...* and select the `optaplanner/build/optaplanner-ide-config/src/main/resources/eclipse.importorder`. . Click *Apply and Close*. === IntelliJ IDEA setup .Prerequisites * The OptaPlanner repository is cloned on your computer. Complete the following steps to configure IntelliJ IDEA: .Procedure . Open the *Settings* or *Preferences* window: * For Windows and Linux, select menu:File[Settings]. * For macOS, select menu:IntelliJ IDEA[Preferences]. . Navigate to *Plugins* and install the https://plugins.jetbrains.com/plugin/6546-eclipse-code-formatter[Eclipse Code Formatter Plugin] from the Marketplace. . Restart your IDE. . Open the *Settings* (or *Preferences*) window again and navigate to menu:Other Settings[Eclipse Code Formatter]. . Configure the Eclipse Code Formatter: .. Select *Use the Eclipse Code Formatter* to enable the plugin. .. In the *Eclipse formatter config* section, select the *Eclipse workspace/project folder or config file* option, click *Browse...* and then select `optaplanner/build/optaplanner-ide-config/src/main/resources/eclipse-format.xml`. .. Make sure the *Optimize Imports* box is ticked, then select the *From file* option and browse for `optaplanner/build/optaplanner-ide-config/src/main/resources/eclipse.importorder`. ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/development-guide.adoc ================================================ [[development-guide]] = Development guide == Project structure The project is a multi-module Maven project. .Module dependency tree diagram image::modules.dot.svg[align="center"] At the bottom of the module tree there are the back end and front end modules, which contain the application source code. The standalone module is an assembly module that combines the back end and front end into a single executable JAR file. The distribution module represents the final assembly step. It takes the standalone application and the documentation and wraps them in an archive that is easy to distribute. == Developing OptaWeb Vehicle Routing The back end and front end are separate projects that can be built and deployed separately. In fact, they are written in completely different languages and built with different tools. Both projects have tools that provide a modern developer experience with fast turn-around between code changes and the running application. In the next sections you will learn how to run both back end and front end projects in development mode. [[backend]] == Back end //// - OptaPlanner, GraphHopper - Quarkus - Configuration (`application.properties`, `application-*.properties`) - Package structure - DevTools - Docker //// The back end module contains a server-side application that uses OptaPlanner to *optimize vehicle routes*. Optimization is a CPU-intensive computation that must avoid any I/O operations in order to perform to its full potential. Because one of the chief objectives is to minimize the travel cost, either time or distance, we need to keep the travel cost information in RAM memory. While solving, OptaPlanner needs to know the travel cost between every pair of locations entered by the user. This information is stored in a structure called the _distance matrix_. When a new location is entered, we calculate the travel cost between the new location and every other location that has been entered so far, and store the travel cost in the distance matrix. The travel cost calculation is performed by a routing engine called https://github.com/graphhopper/graphhopper[GraphHopper]. Finally, the back end module implements additional supporting functionality, such as: - persistence, - WebSocket connection for the front end, - data set loading, export, and import. In the next sections you will learn how to configure and run the back end in development mode. To learn more about the back end code architecture, see <>. [[run-quarkus-maven-plugin]] === Running the back end using the Quarkus Maven plugin .Prerequisites - Java 11 or higher is <>. - The data directory is set up. - An OSM file is downloaded. // TODO application-local.properties You can manually <> and <> or you can use the <> to complete these tasks. .Procedure To run the back end in development mode, enter the following command: [source,shell] ---- mvn compile quarkus:dev ---- === Quarkus development mode In development mode, the back end automatically restarts whenever you refresh the browser tab where the front end runs if there are any changes in the back-end source code or configuration. Learn more about https://quarkus.io/guides/maven-tooling#development-mode[Quarkus development mode]. === Running the back end from IntelliJ IDEA Ultimate IntelliJ IDEA Ultimate has a bundled Quarkus plugin that automatically creates run configurations for modules using the Quarkus framework. Use the *optaweb-vehicle-routing-backend* run configuration to run the back end. Learn more about https://www.jetbrains.com/help/idea/quarkus.html#run-app[running Quarkus applications] in IntelliJ IDEA Ultimate. [[backend-configuration]] === Configuration ==== The `application.properties` file The base configuration is stored in the `application.properties` file, under `/src/main/resources/`. This file is under version control. Use it to permanently store the default configuration and to define Quarkus profiles. ==== System properties To override the default configuration temporarily, use system properties (`-Dproperty=value`). ==== The `.env` file To override the default configuration permanently, for example to store a configuration that is specific to your development environment, use the `optaweb-vehicle-routing-backend/.env` file. This file is excluded from version control and so it does not exist when you clone the repository. Use `optaweb-vehicle-routing-backend/.env-example` to initialize your own `optaweb-vehicle-routing-backend/.env` file. You can make changes in the `.env` file without affecting the Git working tree. See the complete list of <>. See also the complete list of https://quarkus.io/guides/all-config[common application properties] available in Quarkus. === Logging OptaWeb uses the SLF4J API and Logback as the logging framework. For more information, see https://quarkus.io/guides/logging[Quarkus - Configuring Logging]. [[frontend]] == Front end //// - PatternFly, Leaflet - Npm, React, Redux, TypeScript, ESLint, Cypress, `ncu` - Chrome, plugins - Docker //// The front end project was bootstrapped with https://create-react-app.dev/[Create React App]. Create React App provides a number of scripts and dependencies that help with development and with building the application for production. === Setting up the development environment .Procedure . On Fedora, run the following command to install npm: + [source,shell] ---- sudo dnf install npm ---- For more information about installing npm, see https://docs.npmjs.com/downloading-and-installing-node-js-and-npm[Downloading and installing Node.js and npm]. === Install npm dependencies Unlike Maven, the npm package manager installs dependencies in `node_modules` under the project directory and does that only when requested by running `npm install`. Whenever the dependencies listed in `package.json` change (for example when you pull changes to the main branch) you must run `npm install` before you run the development server. .Procedure . Change directory to the front end module: + [source,shell] ---- cd optaweb-vehicle-routing-frontend ---- . Install dependencies: + [source,shell] ---- npm install ---- === Running the development server .Prerequisites - npm is installed. - npm dependencies are installed. .Procedure . Run the development server: + [source,shell] ---- npm start ---- . Open http://localhost:3000/ in a web browser. By default, the `npm start` command attempts to open this URL in your default browser. [TIP] .Prevent `npm start` from launching your default browser ==== If you don't want `npm start` to open a new browser tab each time you run it, export an environment variable `BROWSER=none`. You can use `.env.local` file to make this preference permanent. To do that, enter the following command: [source,shell] ---- echo BROWSER=none >> .env.local ---- ==== The browser refreshes the page whenever you make changes in the front end source code. The development server process running in the terminal picks up the changes as well and prints compilation and lint errors to the console. === Running tests .Procedure . Run `npm test`. === Changing the back end location Use an environment variable called `REACT_APP_BACKEND_URL` to change the back end URL when running `npm start` or `npm run build`. For example: [literal] .... REACT_APP_BACKEND_URL=http://10.0.0.123:8081 .... Note that environment variables will be "`baked`" inside the JavaScript bundle during the npm build, so you need to know the back end location before you build and deploy the front end. Learn more about the React environment variables in https://create-react-app.dev/docs/adding-custom-environment-variables/[Adding Custom Environment Variables]. == Building the project Run `./mvnw install` or `mvn install`. ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/index.adoc ================================================ = Using OptaWeb Vehicle Routing The OptaPlanner Team :doctype: book :experimental: :icons: font :sectanchors: :sectlinks: :sectnums: :toc: left :toclevels: 3 [.normal] As a developer, you can use OptaWeb Vehicle Routing to optimize your vehicle fleet deliveries. In this guide, you will create and run a sample OptaWeb Vehicle Routing application. include::introduction.adoc[leveloffset=+1] include::quickstart.adoc[leveloffset=+1] include::run-locally.adoc[leveloffset=+1] include::run-noscript.adoc[leveloffset=+1] include::run-openshift.adoc[leveloffset=+1] include::user-guide.adoc[leveloffset=+1] include::development-guide.adoc[leveloffset=+1] include::contributing.adoc[leveloffset=+1] include::appendix-backend-architecture.adoc[leveloffset=+1] include::appendix-backend-config.adoc[leveloffset=+1] ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/introduction.adoc ================================================ = Introduction == What is OptaWeb Vehicle Routing? The main purpose of many types of businesses is to transport various types of cargo. The goal of these businesses is to deliver a piece of cargo from the loading point to a destination and use its vehicle fleet in the most efficient way. This type of optimization problem is referred to as the https://www.optaplanner.org/learn/useCases/vehicleRoutingProblem.html[vehicle routing problem] (VRP) and has many variations. https://www.optaplanner.org/[OptaPlanner] can solve many of these vehicle routing variations and provides solution examples. OptaPlanner enables developers to focus on modeling business rules and requirements instead of learning https://en.wikipedia.org/wiki/Constraint_programming[constraint programming] theory. OptaWeb Vehicle Routing expands OptaPlanner's vehicle routing capabilities by providing a reference implementation that answers questions such as these: * Where do I get the distances and travel times? * How do I visualize the solution on a map? * How do I build an application that runs in the cloud? ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/modules.dot ================================================ digraph { graph [bgcolor="transparent"] node [fontname="sans-serif", fontsize=18, style=filled, fillcolor="#f8f8f8"]; "distribution" -> "docs"; "distribution" -> "standalone"; "standalone" -> "backend"; "standalone" -> "frontend"; } ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/quickstart.adoc ================================================ = Quickstart You can get up and running with OptaWeb Vehicle Routing in just a few steps. In this chapter you will download the OptaWeb Vehicle Routing distribution archive containing a binary build of OptaWeb Vehicle Routing. You will use a Bash script to run the binary without having to build the project. // TODO make this a prerequisite of build procedure //==== Internet access // //You need internet access when you build and run the application. //The application source code depends on Maven and NPM packages that will be downloaded during build. //When the application runs it uses third party, public services such as link:https://www.openstreetmap.org/about[OpenStreetMap] //to display map tiles and provide search results. [[install-java]] == Install Java 11 or higher *Java SE 11 or higher* must be installed on your system before you can use OptaWeb Vehicle Routing. NOTE: It is recommended that you install Java SE Development Kit (JDK) because it is necessary in order to build OptaWeb Vehicle Routing from the source. However, if you have a binary distribution of OptaWeb Vehicle Routing, you only need the Java SE Runtime Environment (JRE). .Procedure . To verify the current Java installation, enter the following command: + [source,shell] ---- java -version ---- . If necessary, install OpenJDK 11. * To install OpenJDK 11 on Fedora, enter the following command: + [source,shell] ---- sudo dnf install java-11-openjdk-devel ---- * To install OpenJDK on other platforms, follow instructions at https://openjdk.java.net/install/. == Download distribution archive Download the OptaWeb Vehicle Routing distribution archive, available from the OptaPlanner website, to quickly evaluate OptaWeb Vehicle Routing without having to set up build tools. NOTE: If you want to modify OptaWeb Vehicle Routing and build it yourself or contribute to upstream, see <>. .Procedure . Go to https://www.optaplanner.org/download/download.html and click the *OptaWeb Vehicle Routing* tab. . Click *Download OptaWeb Vehicle Routing {revnumber}*. + .OptaPlanner download page image::download.png[OptaPlanner download page at www.optaplanner.org,align="center"] + . Extract the downloaded distribution ZIP file. The archive contains source files and a binary build of OptaWeb Vehicle Routing as well as the OptaWeb Vehicle Routing documentation. + .Content of the OptaWeb Vehicle Routing distribution archive image::distribution.png[Content of the OptaWeb Vehicle Routing distribution archive,align="center"] == Run OptaWeb Vehicle Routing After you download OptaWeb Vehicle Routing and extract the distribution archive, use the `runLocally.sh` script to run it. NOTE: If the standalone JAR is not part of the distribution, build the project from source by using the `sources` directory. You can use the `sources` directory inside the distribution as if you have cloned the source repository from GitHub. // TODO build instructions NOTE: If Bash is not available on your system, continue to <>. .Prerequisites * Internet access is available. When OptaWeb Vehicle Routing runs it uses third-party public services such as link:https://www.openstreetmap.org/about[OpenStreetMap] to display map tiles and provide search results. * Java 11 or higher is installed. * OptaWeb Vehicle Routing distribution archive is downloaded and extracted. .Procedure Enter the following command: [source,bash] ---- ./bin/runLocally.sh ---- The script will download an OSM file that is needed to work with the sample data set that is included with the application. The script also has an interactive mode you can use to download additional regions. See <> to learn more about the script. ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/run-locally.adoc ================================================ [[run-locally-sh]] = Run locally using the script Linux and macOS users can use a Bash script called `runLocally.sh` to run OptaWeb Vehicle Routing. The script automates some setup steps that would otherwise have to be carried out manually. The script will: * Create the data directory. * Download selected OSM files from Geofabrik. * Try to associate a country code with each downloaded OSM file automatically. * Build the project if the standalone JAR file does not exist. * Launch OptaWeb Vehicle Routing by taking a single region argument or by selecting the region interactively. == Quickstart mode In quickstart mode, the script downloads the region that is required to work with the built-in data set. This is the easiest way to get started. To use the quickstart mode, run the script with no arguments. .Prerequisites * `optaweb-vehicle-routing` repository is cloned on your computer. * Internet access is available. * Java 11 or higher is installed. .Procedure . Change directory to the project root. . Run `./runLocally.sh`. . Confirm the download of the OSM file needed to work with the built-in data set. The application starts after the OSM file is downloaded. Open http://localhost:8080 in a web browser to work with OptaWeb Vehicle Routing. NOTE: The first start may take a few minutes because the OSM file needs to be imported by GraphHopper and stored as a road network graph. Subsequent runs will load the graph from the file system without importing the OSM file and will be significantly faster. == Interactive mode Using the interactive mode, you can see the list of downloaded OSM files and country codes assigned to each region. You can use the interactive mode to download additional OSM files from Geofabrik without visiting the website and choosing a destination for the download. === Download a new region using the script .Procedure . Run `./runLocally.sh -i`. . Enter `d` to show the download menu. . Go to a region by entering its ID and then entering `e`. . Repeat the previous step until you see a list with the region you want to download. . Download a region by entering its ID and then entering `d`. [WARNING] .Using large OSM files ==== For the best user experience, use smaller regions such as individual European or US states. Using OSM files larger than 1 GB will require significant RAM size and take a lot of time (up to several hours) for the initial processing. ==== === Select a region and run OptaWeb Vehicle Routing .Procedure . Run `./runLocally.sh -i`. . Select a region from the list of downloaded regions by entering its ID. . Confirm the project build if it hasn't been built yet. . Confirm starting OptaWeb Vehicle Routing using the selected region. == Non-interactive mode Use the non-interactive mode to specify an existing region and start OptaWeb Vehicle Routing with a single command. This is useful for switching between regions quickly or when doing a demo. .Procedure Run `./runLocally.sh `. //// == Air distance mode OptaWeb Vehicle Routing can work in air distance mode that calculates travel times based on the distance between two coordinates. Use this mode in situations where you need to get OptaWeb Vehicle Routing up and running as quickly as possible and do not want to use an OSM (OpenStreetMap) file. Air distance mode is only useful if you need to smoke-test OptaWeb Vehicle Routing and you do not need accurate travel times. .Procedure Run the `runLocally.sh` script with `--air` argument to start OptaWeb Vehicle Routing in air distance mode: [source,bash] ---- ./bin/run.sh --air ---- //// == Tweak the data directory * To use a different data directory, write its absolute path to the `.DATA_DIR_LAST` file at the project root. * To change country codes associated with a region, edit the corresponding file under `_DATA_DIR_/country_codes/`. + For example, you could have downloaded an OSM file for Scotland, for which the script fails to guess the country code. In this case, set the content of `_DATA_DIR_/country_codes/scotland-latest` to `GB`. * To remove a region, delete the corresponding OSM file from `_DATA_DIR_/openstreetmap/` and GraphHopper directory from `_DATA_DIR_/graphhopper/`. ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/run-noscript.adoc ================================================ [[run-noscript]] = Run locally without the script include::attributes.adoc[] Follow this section if you cannot use <> to run OptaWeb Vehicle Routing because Bash is not available on your system. [[download-osm]] == Download routing data The routing engine requires geographical data to calculate the time it takes vehicles to travel between locations. You must download and store OSM (OpenStreetMap) data files on the local file system before you run OptaWeb Vehicle Routing. NOTE: The OSM data files are typically between 100 MB to 1 GB and take time to download so it is a good idea to download the files before building or starting the OptaWeb Vehicle Routing application. .Procedure . Open https://download.geofabrik.de/ in a web browser. . Click a region in the *Sub Region* list, for example *Europe*. The subregion's page opens. . In the *Sub Regions* table, download the OSM file (`.osm.pbf`) for a country, for example Belgium. [[data-dir-setup]] == Create data directory structure OptaWeb Vehicle Routing reads and writes several types of data on the file system. It reads OSM (OpenStreetMap) files from the `openstreetmap` directory, writes a road network graph to the `graphhopper` directory, and persists user data in a directory called `db`. Create a new directory dedicated to storing all of these data to make it easier to upgrade to a newer version of OptaWeb Vehicle Routing in the future and continue working with the data you created previously. .Procedure . Create the `openstreetmap` directory in your user account `home` directory, for example: + [source,subs="attributes+"] ---- $HOME/{data-dir-name} └── openstreetmap ---- . Move all of your downloaded OSM files (files with the extension `.osm.pbf`) to the `openstreetmap` directory. The rest of the directory structure will be created by the OptaWeb Vehicle Routing application when it runs for the first time. After that, your directory structure will look similar to the following example: // TODO maybe replace this with a screenshot, doesn't look good in PDF. [source,subs="attributes+"] ---- $HOME/{data-dir-name} ├── db │ └── vrp.mv.db ├── graphhopper │ └── belgium-latest └── openstreetmap └── belgium-latest.osm.pbf ---- == Run using the `java` command .Prerequisites * Internet access is available. When OptaWeb Vehicle Routing runs it uses third-party public services such as link:https://www.openstreetmap.org/about[OpenStreetMap] to display map tiles and provide search results. * Java 11 or higher is installed. * The data directory is created at `$HOME/{data-dir-name}`. * A subdirectory called `openstreetmap` with at least one OSM file exists. * A country code to use in search queries is identified. .Procedure Enter the following command: [source,subs="attributes+"] ---- java \ -Dapp.demo.data-set-dir=$HOME/{data-dir-name}/dataset \ -Dapp.persistence.h2-dir=$HOME/{data-dir-name}/db \ -Dapp.routing.gh-dir=$HOME/{data-dir-name}/graphhopper \ -Dapp.routing.osm-dir=$HOME/{data-dir-name}/openstreetmap \ -Dapp.routing.osm-file=belgium-latest.osm.pbf \ -Dapp.region.country-codes=BE \ -jar optaweb-vehicle-routing-standalone/target/quarkus-app/quarkus-run.jar ---- ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/run-openshift.adoc ================================================ [[run-openshift]] = Run on OpenShift Linux and macOS users can use the `runOnOpenShift.sh` Bash script to install OptaWeb Vehicle Routing on OpenShift. == Running on a local OpenShift cluster Use https://developers.redhat.com/products/codeready-containers[Red Hat CodeReady Containers] to easily set up a single-node OpenShift 4 cluster on your local computer. .Prerequisites You have successfully built the project with Maven. .Procedure . To install CRC, follow the link:https://code-ready.github.io/crc/[Red Hat CodeReady Containers Getting Started Guide]. . When the cluster starts, perform the following steps: .. Add the OpenShift command-line interface (`oc`) to your `$PATH`: + [source,shell] ---- eval $(crc oc-env) ---- .. Log in as `developer`: + [source,shell] ---- oc login -u developer -p developer https://api.crc.testing:6443 ---- .. Create a new project: + [source,subs="quotes"] ---- oc new-project _project_name_ ---- .. Run the script: + [source,subs="quotes"] ---- ./runOnOpenShift.sh _osm_file_name_ _country_code_list_ _osm_file_download_url_ ---- .. Enter the following command for information about how to use the script: + [source,shell] ---- ./runOnOpenShift.sh --help ---- === Updating the deployed application with local changes ==== Back end . Change the source code and build the back end module with Maven. . Start OpenShift build: [source,shell] ---- cd optaweb-vehicle-routing-backend oc start-build backend --from-dir=. --follow ---- ==== Front end . Change the source code and build the front end module with npm. . Start OpenShift build: [source,shell] ---- cd optaweb-vehicle-routing-frontend oc start-build frontend --from-dir=docker --follow ---- ================================================ FILE: optaweb-vehicle-routing-docs/src/main/asciidoc/user-guide.adoc ================================================ = Using OptaWeb Vehicle Routing In the OptaWeb Vehicle Routing application, you can mark a number of locations on the map. The first location is assumed to be the depot. Vehicles must deliver goods from this depot to every other location that you marked. You can set the number of vehicles and the carrying capacity of every vehicle. However, the route is not guaranteed to use all vehicles. The application uses as many vehicles as required for an optimal route. The current version has certain limitations: * Every delivery to a location is supposed to take 1 point of vehicle capacity. For example, a vehicle with a capacity of 10 can visit up to 10 locations before returning to the depot. * Setting custom names of vehicles and locations is not supported. == Creating a route To create an optimal route, use the *Demo* tab of the user interface. . Click *Demo* to open the *Demo* tab. . Use the blue icon:minus-square[role="blue"] and icon:plus-square[role="blue"] buttons above the map to set the number of vehicles. Each vehicle has a default capacity of 10. . Use the icon:plus-square-o[] button on the map to zoom in as necessary. + [NOTE] ==== Do not double-click to zoom in. A double click also creates a location. ==== + . Click a location for the depot. . Click other locations on the map for delivery points. . If you want to delete a location: .. Hover the mouse cursor over the location to see the location name. .. Find the location name in the list in the left part of the screen. .. Click the icon:times[role="blue"] icon next to the name. Every time you add or remove a location or change the number of vehicles, the application creates and displays a new optimal route. If the solution uses several vehicles, the application shows the route for every vehicle in a different color. == Viewing and setting other details You can use other tabs of the user interface to view and set additional details. * In the *Vehicles* tab, you can view, add, and remove vehicles, and also set the capacity for every vehicle. * In the *Visits* tab, you can view and remove locations. * In the *Route* tab, you can select every vehicle and view the route for this vehicle. [[creating-custom-data-sets]] == Creating custom data sets There is a built-in demo data set consisting of a several large Belgian cities. If you want to have more demos offered by the *Load demo* dropdown, you can prepare your own data sets. To do that, follow these steps: . Add a depot and a number of visits by clicking on the map or using geosearch. . Click *Export* and save the file in the _data set_ directory. + [NOTE] ==== Data set directory is where the `app.demo.data-set-dir` property points to. If the application is running through the <>, it will be set to `$HOME/{data-dir-name}/dataset`. Otherwise, the property will be taken from `application.properties` and defaults to `optaweb-vehicle-routing-backend/local/dataset`. ==== . Edit the YAML file and choose a unique name for the data set. . Restart the back end. After you restart the back end, files in the _data set_ directory will be made available in the *Load demo* dropdown. == Troubleshooting If the application behaves unexpectedly, review the back end terminal output log. To resolve issues, remove the back end database: . Stop the back end by pressing kbd:[Ctrl+C] in the back end terminal window. . Remove the directory `optaweb-vehicle-routing/optaweb-vehicle-routing-backend/local/db`. ================================================ FILE: optaweb-vehicle-routing-docs/src/main/assembly/assembly-generated-docs-zip.xml ================================================ zip-with-generated-docs zip false ${project.build.directory}/generated-docs/html_single/ html_single/ ${project.build.directory}/generated-docs/pdf/optaweb-vehicle-routing-docs.pdf pdf/ ================================================ FILE: optaweb-vehicle-routing-frontend/.editorconfig ================================================ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true # Matches multiple files with brace expansion notation # Set default charset [*.{js,py}] charset = utf-8 # Indentation override for all JS under lib directory [src/**.js] indent_style = space indent_size = 2 # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_style = space indent_size = 2 ================================================ FILE: optaweb-vehicle-routing-frontend/.eslintignore ================================================ /cypress/support /cypress/plugins registerServiceWorker.ts ================================================ FILE: optaweb-vehicle-routing-frontend/.eslintrc.json ================================================ { "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json" }, "plugins": [ "@typescript-eslint", "jest", "jest-dom" ], "env": { "browser": true, "jest/globals": true, "node": true }, "settings": { "react": { "version": "detect" }, "linkComponents": [ // Components used as alternatives to for linking, eg. "Hyperlink", {"name": "Link", "linkAttribute": "to"} ] }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:jest-dom/recommended", "react-app", "airbnb-typescript" ], "rules": { "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], "@typescript-eslint/indent": ["error", 2], "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-use-before-define": "off", "import/no-extraneous-dependencies": [ "error", { "devDependencies": [ "**/*.test.ts", "**/*.test.tsx", "src/store/mockStore.ts", "src/setupTests.ts" ] } ], "import/prefer-default-export": "off", "max-len": ["error", {"code": 120}], "no-console": "warn", "object-curly-newline": [ "error", { "ImportDeclaration": {"multiline": true}, "ExportDeclaration": {"multiline": true} } ], "react/destructuring-assignment": "off", "react/jsx-props-no-spreading": "off", "react/prop-types": "off", "react/react-in-jsx-scope": "off", "react/sort-comp": "off" } } ================================================ FILE: optaweb-vehicle-routing-frontend/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. /target /local # dependencies /node_modules # frontend-maven-plugin /node # testing /coverage # production /build # .env files .env.local .env.development.local .env.test.local .env.production.local # logs npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: optaweb-vehicle-routing-frontend/.prettierrc ================================================ { "singleQuote": true, "trailingComma": "es5" } ================================================ FILE: optaweb-vehicle-routing-frontend/README.adoc ================================================ :david-project: https://david-dm.org/kiegroup/optaweb-vehicle-routing :david-path: path=optaweb-vehicle-routing-frontend :david-deps: {david-project}/status.svg?{david-path} :david-devDeps: {david-project}/dev-status.svg?{david-path} :david-link: {david-project}?{david-path} [[optaweb-vehicle-routing-frontend]] = OptaWeb Vehicle Routing front end image:{david-deps}["dependencies Status",link="{david-link}"] image:{david-devDeps}["devDependencies Status",link="{david-link}&type=dev"] [[available-scripts]] == Available scripts In the project directory, you can run: [[npm-start]] === `npm start` Runs the app in the development mode. Open http://localhost:3000 to view it in the browser. The page will reload if you make edits. You will also see any lint errors in the console. [[npm-test]] === `npm test` Launches the test runner in the interactive watch mode. See the section about https://create-react-app.dev/docs/running-tests/[running tests] for more information. [[npm-run-lint]] === `npm run lint` Check the whole `src/` directory for linter errors. Some IDEs are difficult to be configured to be 100% compliant with the project linter configuration. Use `npm run lint:fix` to resolve the edge cases that your IDE cannot handle. [[npm-run-build]] === `npm run build` Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance. See the section about https://create-react-app.dev/docs/deployment/[deployment] for more information. [[browserslist-support]] == Browserslist support Developers set versions list in queries like `last 2 version` to be free from updating versions manually. Browserslist will use http://caniuse.com/[Can I Use] data for this queries. Check the project current browser query on https://browserl.ist/?q=%3E0.2%25%2C+not+dead%2C+not+ie%3C%3D11%2Cnot+op_mini+all[browserl.ist] or by running `npx browserslist`. Example: [source,json] ---- { "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] } ---- [[learn-more]] == Learn more To learn React, check out the https://reactjs.org/[React documentation]. This project was bootstrapped with https://github.com/facebook/create-react-app[Create React App]. Learn more on https://github.com/browserslist/browserslist#readme[Browserslist]. ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/.eslintrc.json ================================================ { "plugins": [ "cypress" ], "env": { "cypress/globals": true } } ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/.gitignore ================================================ screenshots videos ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/fixtures/example.json ================================================ { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" } ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/fixtures/response-garz.json ================================================ [ { "place_id": 1243027, "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", "osm_type": "node", "osm_id": 311384628, "boundingbox": [ "53.0183353", "53.0583353", "12.0651506", "12.1051506" ], "lat": "53.0383353", "lon": "12.0851506", "display_name": "Garz, Hoppenrade, Plattenburg, Prignitz, Brandenburg, Germany", "class": "place", "type": "hamlet", "importance": 0.35, "icon": "https://nominatim.openstreetmap.org/ui/mapicons//poi_place_village.p.20.png" } ] ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/fixtures/response-hoppenrade.json ================================================ [ { "place_id": 283087710, "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", "osm_type": "relation", "osm_id": 9656282, "boundingbox": [ "53.0147994", "53.0556191", "12.0157931", "12.1143161" ], "lat": "53.03502605", "lon": "12.069659320997278", "display_name": "Hoppenrade, Plattenburg, Prignitz, Brandenburg, Germany", "class": "boundary", "type": "administrative", "importance": 0.4, "icon": "https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png" }, { "place_id": 536362, "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", "osm_type": "node", "osm_id": 240025900, "boundingbox": [ "53.019054", "53.059054", "12.042413", "12.082413" ], "lat": "53.039054", "lon": "12.062413", "display_name": "Hoppenrade, Plattenburg, Prignitz, Brandenburg, 19339, Germany", "class": "place", "type": "hamlet", "importance": 0.35, "icon": "https://nominatim.openstreetmap.org/ui/mapicons//poi_place_village.p.20.png" } ] ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/integration/fromLocationsToRoute.js ================================================ describe('Locations can be added and route is computed', () => { const cities = ['Garz', 'Hoppenrade']; /** * Adds a location by searching for a city of a given name. * @param { string } name - city name */ const addCity = (name) => { cy.get('[data-cy=geosearch-text-input]').type(name); // TODO: replace by mocking the request (search depends on a 3rd-party service) cy.get('[data-cy=geosearch-location-item-button-0]', { timeout: 60000 }).click(); }; /** * Clears locations by clicking on the 'Clear' button. */ const clearLocations = () => { // Add one city to make sure there is a location in the list and the clear button shows up addCity('Garz'); cy.get('[data-cy=demo-clear-button]').click({ force: true }); cy.wait('@postClear'); }; /** * Waits for a websocket connection to be established. */ const visitDemo = () => { cy.visit('/'); cy.get('a[href="/demo"]').click(); }; before(() => { cy.intercept('POST', '**/api/clear').as('postClear'); cities.forEach((city) => cy.intercept( 'GET', `https://nominatim.openstreetmap.org/search?*q=${city}`, { fixture: `response-${city.toLowerCase()}.json` }, )); visitDemo(); clearLocations(); }); it('Locations added via clicking on a map are added to a route', () => { cities.forEach((city) => { addCity(city); }); cy.get('[data-cy=demo-add-vehicle]').click(); cy.get('a[href="/routes"]').click(); cy.get('[data-cy=location-list]').find('li').should((list) => { cities.forEach((city) => expect(list).to.contain(city)); }); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/plugins/index.js ================================================ // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config } ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) ================================================ FILE: optaweb-vehicle-routing-frontend/cypress/support/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** import './commands'; // Import commands.js using ES2015 syntax: // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: optaweb-vehicle-routing-frontend/cypress.json ================================================ { "baseUrl": "http://localhost:3000", "reporter": "junit", "reporterOptions": { "mochaFile": "target/test-reports/TEST-cypress.xml", "jenkinsMode": true } } ================================================ FILE: optaweb-vehicle-routing-frontend/docker/.gitignore ================================================ build ================================================ FILE: optaweb-vehicle-routing-frontend/docker/Dockerfile ================================================ FROM docker.io/library/nginx:1.17.5 COPY nginx.conf /etc/nginx COPY default.conf /tmp/default.template ARG BACKEND_URL=http://backend:8080 RUN envsubst '${BACKEND_URL}' < /tmp/default.template > /etc/nginx/conf.d/default.conf \ && rm /tmp/default.template \ # Make directories used by nginx owned and writable by the root group to support arbitrary user ID. # See: https://docs.openshift.com/container-platform/4.2/openshift_images/create-images.html && chgrp 0 /var/cache/nginx/ \ && chmod g=u /var/cache/nginx/ \ && chgrp 0 /var/run/ \ && chmod g=u /var/run/ COPY build /usr/share/nginx/html EXPOSE 8080 ================================================ FILE: optaweb-vehicle-routing-frontend/docker/default.conf ================================================ server { listen 8080; server_name localhost; #charset koi8-r; #access_log /var/log/nginx/host.access.log main; location / { root /usr/share/nginx/html; index index.html index.htm; # Workaround for client-side routing: # - https://create-react-app.dev/docs/deployment/#serving-apps-with-client-side-routing # - https://stackoverflow.com/questions/43951720/react-router-and-nginx try_files $uri /index.html; } location /api { proxy_pass ${BACKEND_URL}/api; } # EventSource configuration: # - https://stackoverflow.com/questions/13672743/eventsource-server-sent-events-through-nginx location /api/events { proxy_pass ${BACKEND_URL}/api/events; proxy_set_header Connection ''; proxy_http_version 1.1; chunked_transfer_encoding off; proxy_buffering off; proxy_cache off; proxy_read_timeout 40h; # Otherwise the connection will be closed after 60s. } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } ================================================ FILE: optaweb-vehicle-routing-frontend/docker/nginx.conf ================================================ worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } ================================================ FILE: optaweb-vehicle-routing-frontend/package.json ================================================ { "name": "optaweb-vehicle-routing-frontend", "homepage": ".", "private": true, "license": "Apache-2.0", "dependencies": { "@patternfly/patternfly": "~4.219.2", "@patternfly/react-core": "~4.250.1", "@patternfly/react-icons": "~4.92.10", "leaflet": "^1.6.0", "leaflet-geosearch": "^3.7.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-leaflet": "^2.7.0", "react-redux": "^8.0.5", "react-router": "^5.3.4", "react-router-dom": "^5.3.4", "react-scripts": "^5.0.1", "redux": "^4.2.0", "redux-devtools-extension": "^2.13.8", "redux-logger": "^3.0.6", "redux-thunk": "^2.4.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "postbuild": "shx rm -rf docker/build && shx cp -r build docker", "test": "react-scripts test --reporters=default --reporters=jest-junit", "eject": "react-scripts eject", "coverage": "npm run test -- --coverage --watchAll=false", "update-snapshots": "npm run test -- -u --watchAll=false", "typecheck": "tsc --noEmit", "lint": "eslint --ext .js,.ts,.tsx src/ cypress/", "lint:fix": "npm run lint -- --fix", "cypress:open": "cypress open", "cypress:run": "cypress run" }, "jest-junit": { "outputDirectory": "./target/test-reports", "outputName": "TEST-frontend.xml", "suiteName": "org.optaweb.vehiclerouting.frontend.tests", "suiteNameTemplate": "{filepath}", "classNameTemplate": "org.optaweb.vehiclerouting.frontend.{filename}.{classname}", "titleTemplate": "{title}", "ancestorSeparator": "." }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "@types/jest": "^27.5.2", "@types/leaflet": "^1.5.12", "@types/node": "^13.13.5", "@types/react": "~18.0.25", "@types/react-dom": "^18.0.9", "@types/react-leaflet": "^2.5.1", "@types/react-router-dom": "^5.3.3", "@types/react-test-renderer": "^18.0.0", "@types/redux-logger": "^3.0.9", "@types/redux-mock-store": "^1.0.3", "cypress": "^7.0.1", "eslint-config-airbnb-typescript": "~17.0.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-jest-dom": "^4.0.3", "eventsourcemock": "^2.0.0", "fetch-mock-jest": "^1.5.1", "history": "^4.10.1", "jest-junit": "^10.0.0", "node-fetch": "^2.6.1", "react-test-renderer": "^18.2.0", "redux-mock-store": "^1.5.4", "shx": "^0.3.4", "typescript": "~4.1", "use-sync-external-store": "^1.2.0" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: optaweb-vehicle-routing-frontend/pom.xml ================================================ 4.0.0 org.optaweb.vehiclerouting optaweb-vehicle-routing 8.35.0.Final optaweb-vehicle-routing-frontend war OptaWeb Vehicle Routing Frontend src **/*test.ts,**/*test.tsx coverage/lcov.info optaweb-vehicle-routing-frontend com.github.eirslett frontend-maven-plugin ${version.frontend-maven-plugin} ${version.node} ${version.npm} com.github.eirslett frontend-maven-plugin install node and npm initialize install-node-and-npm npm install compile npm install 0 npm run typecheck compile npm run typecheck npm run lint compile npm run lint npm test test npm run coverage maven-resources-plugin copy-resources prepare-package copy-resources ${project.build.directory}/${project.build.finalName} build npmBuild . com.github.eirslett frontend-maven-plugin npm run build prepare-package npm run build productized productized com.github.eirslett frontend-maven-plugin lock-treatment-tool execution initialize npm exec @kie/lock-treatment-tool@^0.2.2 -- lock-treatment-tool final cleanup prepare-package npm exec @kie/lock-treatment-tool@^0.2.2 -- ================================================ FILE: optaweb-vehicle-routing-frontend/public/index.html ================================================ OptaWeb Vehicle Routing

================================================ FILE: optaweb-vehicle-routing-frontend/public/manifest.json ================================================ { "short_name": "OptaWeb VRP", "name": "OptaWeb Vehicle Routing", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: optaweb-vehicle-routing-frontend/src/@types/eventsourcemock.d.ts ================================================ // See https://github.com/gcedo/eventsourcemock declare module 'eventsourcemock' { interface EventSource { onopen: () => unknown; onerror: () => unknown; emit(eventName: string, messageEvent?: MessageEvent); emitOpen(); } let sources: { [key: string]: EventSource }; } ================================================ FILE: optaweb-vehicle-routing-frontend/src/common.ts ================================================ export const backendUrl = process.env.REACT_APP_BACKEND_URL; ================================================ FILE: optaweb-vehicle-routing-frontend/src/index.css ================================================ body { margin: 0; padding: 0; font-family: sans-serif; } ================================================ FILE: optaweb-vehicle-routing-frontend/src/index.tsx ================================================ import '@patternfly/react-core/dist/styles/base.css'; import { backendUrl } from 'common'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import './index.css'; import { unregister } from './registerServiceWorker'; import { configureStore } from './store'; import App from './ui/App'; const store = configureStore({ backendUrl: `${backendUrl}/api`, }); ReactDOM.render( , document.getElementById('root') as HTMLElement, ); unregister(); ================================================ FILE: optaweb-vehicle-routing-frontend/src/react-app-env.d.ts ================================================ /* eslint-disable spaced-comment */ /// ================================================ FILE: optaweb-vehicle-routing-frontend/src/registerServiceWorker.ts ================================================ // In production, we register a service worker to serve assets from local cache. // This lets the app load faster on subsequent visits in production, and gives // it offline capabilities. However, it also means that developers (and users) // will only see deployed updates on the 'N+1' visit to a page, since previously // cached resources are updated in the background. // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. // This link also includes instructions on opting out of this behavior. const isLocalhost = Boolean( window.location.hostname === 'localhost' || // [::1] is the IPv6 localhost address. window.location.hostname === '[::1]' || // 127.0.0.1/8 is considered localhost for IPv4. window.location.hostname.match( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, ), ); export default function register() { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL( process.env.PUBLIC_URL!, window.location.toString(), ); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 return; } window.addEventListener('load', () => { const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { // This is running on localhost. Lets check if a service worker still exists or not. checkValidServiceWorker(swUrl); // Add some additional logging to localhost, pointing developers to the // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { console.log( 'This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://goo.gl/SC7cgQ', ); }); } else { // Is not local host. Just register service worker registerValidSW(swUrl); } }); } } function registerValidSW(swUrl: string) { navigator.serviceWorker .register(swUrl) .then((registration) => { registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker) { installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the old content will have been purged and // the fresh content will have been added to the cache. // It's the perfect time to display a 'New content is // available; please refresh.' message in your web app. console.log('New content is available; please refresh.'); } else { // At this point, everything has been precached. // It's the perfect time to display a // 'Content is cached for offline use.' message. console.log('Content is cached for offline use.'); } } }; } }; }) .catch((error) => { console.error('Error during service worker registration:', error); }); } function checkValidServiceWorker(swUrl: string) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl) .then((response) => { // Ensure service worker exists, and that we really are getting a JS file. if ( response.status === 404 || response.headers.get('content-type')!.indexOf('javascript') === -1 ) { // No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then((registration) => { registration.unregister().then(() => { window.location.reload(); }); }); } else { // Service worker found. Proceed as normal. registerValidSW(swUrl); } }) .catch(() => { console.log( 'No internet connection found. App is running in offline mode.', ); }); } export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then((registration) => { registration.unregister(); }); } } ================================================ FILE: optaweb-vehicle-routing-frontend/src/setupTests.ts ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; import EventSource from 'eventsourcemock'; Object.defineProperty(window, 'EventSource', { value: EventSource, }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/client/actions.ts ================================================ import { Viewport } from 'react-leaflet'; import { ActionFactory } from '../types'; import { ActionType, ResetViewportAction, UpdateViewportAction } from './types'; export const updateViewport: ActionFactory = (viewport) => ({ type: ActionType.UPDATE_VIEWPORT, value: viewport, }); export const resetViewport: ActionFactory = () => ({ type: ActionType.RESET_VIEWPORT, }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/client/client.test.ts ================================================ import { Viewport } from 'react-leaflet'; import * as actions from './actions'; import reducer from './index'; import { initialViewportState } from './reducers'; import { UserViewport } from './types'; const zoom = 13; const center: [number, number] = [1, -3]; const userViewport: UserViewport = { isDirty: true, zoom, center, }; describe('Client reducer', () => { it('update viewport', () => { expect( reducer(initialViewportState, actions.updateViewport({ zoom, center })), ).toEqual(userViewport); expect( reducer(initialViewportState, actions.updateViewport({ zoom: null, center })), ).toEqual(initialViewportState); expect( reducer(initialViewportState, actions.updateViewport({ zoom, center: null })), ).toEqual(initialViewportState); expect( reducer(initialViewportState, actions.updateViewport(null as unknown as Viewport)), ).toEqual(initialViewportState); }); it('reset viewport', () => { expect( reducer(userViewport, actions.resetViewport()), ).toEqual(initialViewportState); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/client/index.ts ================================================ import * as clientOperations from './operations'; import { clientReducer } from './reducers'; export { clientOperations }; export default clientReducer; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/client/operations.ts ================================================ import * as actions from './actions'; export const { updateViewport, resetViewport } = actions; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/client/reducers.ts ================================================ import { ActionType, UserViewport, ViewportAction } from './types'; export const initialViewportState: UserViewport = { isDirty: false, center: [0, 0], zoom: 2, }; // eslint-disable-next-line @typescript-eslint/default-param-last export const clientReducer = (state = initialViewportState, action: ViewportAction): UserViewport => { switch (action.type) { case ActionType.UPDATE_VIEWPORT: { if (!action.value || !action.value.zoom || !action.value.center) { return state; } return { isDirty: true, zoom: action.value.zoom, center: action.value.center }; } case ActionType.RESET_VIEWPORT: { return initialViewportState; } default: return state; } }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/client/types.ts ================================================ import { Viewport as LeafletViewport } from 'react-leaflet'; import { Action } from 'redux'; export enum ActionType { UPDATE_VIEWPORT = 'UPDATE_VIEWPORT', RESET_VIEWPORT = 'RESET_VIEWPORT', } export interface UpdateViewportAction extends Action { value: LeafletViewport; } export interface ResetViewportAction extends Action { } export interface UserViewport { isDirty: boolean; center: [number, number]; zoom: number; } export type ViewportAction = | UpdateViewportAction | ResetViewportAction; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/demo/actions.ts ================================================ import { ActionFactory } from '../types'; import { ActionType, FinishLoadingAction, RequestDemoAction } from './types'; export const requestDemo: ActionFactory = (name) => ({ type: ActionType.REQUEST_DEMO, name, }); export const finishLoading: ActionFactory = () => ({ type: ActionType.FINISH_LOADING, }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/demo/demo.test.ts ================================================ import { mockStore } from '../mockStore'; import { Vehicle } from '../route/types'; import { AppState } from '../types'; import { WebSocketConnectionStatus } from '../websocket/types'; import * as actions from './actions'; import reducer, { demoOperations } from './index'; import { Demo } from './types'; describe('Demo operations', () => { it('demo request should call loadDemo() on client', () => { const { store, client } = mockStore(state); const demoName = 'demo name'; // verify requestDemo operation calls the client store.dispatch(demoOperations.requestDemo(demoName)); expect(client.loadDemo).toHaveBeenCalledTimes(1); expect(store.getActions()).toEqual([actions.requestDemo(demoName)]); }); }); describe('Demo reducers', () => { it('request demo', () => { const demoName = 'some name'; const initialState: Demo = { isLoading: false, demoName: null }; const expectedState: Demo = { isLoading: true, demoName }; expect( reducer(initialState, actions.requestDemo(demoName)), ).toEqual(expectedState); }); it('start loading when loading requested by someone else', () => { const demoName = 'some name'; const initialState: Demo = { isLoading: false, demoName: null }; const expectedState: Demo = { isLoading: true, demoName }; expect( reducer(initialState, actions.requestDemo(demoName)), ).toEqual(expectedState); }); it('loading flag should be cleared when demo is loaded', () => { const demoName = 'some name'; const initialState: Demo = { isLoading: true, demoName }; const expectedState: Demo = { isLoading: false, demoName }; expect( reducer(initialState, actions.finishLoading()), ).toEqual(expectedState); }); }); const vehicle1: Vehicle = { id: 1, name: 'v1', capacity: 5 }; const visit1 = { id: 1, lat: 1.345678, lng: 1.345678, }; const visit2 = { id: 2, lat: 2.345678, lng: 2.345678, }; const visit3 = { id: 3, lat: 3.676111, lng: 3.568333, }; const state: AppState = { connectionStatus: WebSocketConnectionStatus.CLOSED, messages: [], serverInfo: { boundingBox: null, countryCodes: [], demos: [], }, userViewport: { isDirty: false, zoom: 1, center: [0, 0], }, demo: { demoName: null, isLoading: false, }, plan: { distance: '10', vehicles: [vehicle1], depot: null, visits: [visit1, visit2, visit3], routes: [{ vehicle: vehicle1, visits: [visit1, visit2, visit3], track: [[0.111222, 0.222333], [0.444555, 0.555666]], }], }, }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/demo/index.ts ================================================ import * as demoOperations from './operations'; import { demoReducer } from './reducers'; export { demoOperations }; export default demoReducer; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/demo/operations.ts ================================================ import { ThunkCommandFactory } from '../types'; import * as actions from './actions'; import { RequestDemoAction } from './types'; export const { finishLoading } = actions; export const requestDemo: ThunkCommandFactory = ( (name) => (dispatch, _getState, client) => { dispatch(actions.requestDemo(name)); client.loadDemo(name); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/demo/reducers.ts ================================================ import { ActionType, Demo, DemoAction } from './types'; const initialState: Demo = { isLoading: false, demoName: null, }; // eslint-disable-next-line @typescript-eslint/default-param-last export const demoReducer = (state = initialState, action: DemoAction): Demo => { switch (action.type) { case ActionType.REQUEST_DEMO: { return { ...initialState, isLoading: true, demoName: action.name }; } case ActionType.FINISH_LOADING: { return { ...state, isLoading: false }; } default: return state; } }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/demo/types.ts ================================================ import { Action } from 'redux'; export enum ActionType { REQUEST_DEMO = 'REQUEST_DEMO', FINISH_LOADING = 'FINISH_LOADING', } export interface RequestDemoAction extends Action { readonly name: string; } export interface FinishLoadingAction extends Action { } export type DemoAction = | RequestDemoAction | FinishLoadingAction; export interface Demo { readonly isLoading: boolean; readonly demoName: string | null; } ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/index.ts ================================================ export * from './store'; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/message/actions.ts ================================================ import { ActionFactory } from '../types'; import { ActionType, MessagePayload, ReadMessageAction, ReceiveMessageAction } from './types'; export const receiveMessage: ActionFactory = (payload) => ({ type: ActionType.RECEIVE_MESSAGE, payload, }); export const readMessage: ActionFactory = (id) => ({ type: ActionType.READ_MESSAGE, id, }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/message/index.ts ================================================ import * as messageActions from './actions'; import { messageReducer } from './reducers'; import * as messageSelectors from './selectors'; export { messageActions, messageSelectors }; export default messageReducer; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/message/message.test.ts ================================================ import { Message, MessagePayload } from 'store/message/types'; import * as actions from './actions'; import reducer, { messageSelectors } from './index'; const messages: Message[] = [ { id: '1', text: '', status: 'read' }, { id: '2', text: '', status: 'new' }, { id: '3', text: '', status: 'read' }, { id: '4', text: '', status: 'new' }, { id: '5', text: '', status: 'new' }, ]; describe('Message reducer', () => { it( 'should return initial state when previous state is undefined', () => expect(reducer(undefined, actions.readMessage(''))).toEqual([]), ); it('receiving a message should add it with the status equal to NEW', () => { const message: MessagePayload = { id: 'xy', text: 'message text' }; expect( reducer([], actions.receiveMessage(message)), ).toEqual([{ ...message, status: 'new' }]); }); it('reading a message should update its status to READ', () => { const updatedMessages = [...messages]; updatedMessages[1] = { ...messages[1], status: 'read' }; expect( reducer(messages, actions.readMessage('2')), ).toEqual(updatedMessages); }); }); describe('Message selectors', () => { it('select new messages', () => { expect( messageSelectors.getNewMessages(messages), ).toEqual([ { id: '2', text: '', status: 'new' }, { id: '4', text: '', status: 'new' }, { id: '5', text: '', status: 'new' }, ]); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/message/reducers.ts ================================================ import { ActionType, Message, MessageAction } from './types'; // eslint-disable-next-line @typescript-eslint/default-param-last export const messageReducer = (state: Message[] = [], action: MessageAction): Message[] => { switch (action.type) { case ActionType.RECEIVE_MESSAGE: { return [...state, { ...action.payload, status: 'new' }]; } case ActionType.READ_MESSAGE: { return state.map((message) => ( message.id === action.id ? { ...message, status: 'read' } : message )); } default: return state; } }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/message/selectors.ts ================================================ import { Message } from 'store/message/types'; export const getNewMessages = (messages: Message[]) => messages.filter((message) => message.status === 'new'); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/message/types.ts ================================================ import { Action } from 'redux'; export enum ActionType { RECEIVE_MESSAGE = 'RECEIVE_MESSAGE', READ_MESSAGE = 'READ_MESSAGE', } export interface ReceiveMessageAction extends Action { readonly payload: MessagePayload; } export interface ReadMessageAction extends Action { readonly id: string; } export type MessageAction = ReceiveMessageAction | ReadMessageAction; export interface MessagePayload { readonly id: string; readonly text: string; } export interface Message extends MessagePayload { readonly status: 'new' | 'read'; } ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/mockStore.ts ================================================ import { Middleware } from 'redux'; import createMockStore, { MockStoreCreator } from 'redux-mock-store'; import thunk, { ThunkDispatch } from 'redux-thunk'; import WebSocketClient from '../websocket/WebSocketClient'; import { UpdateRouteAction } from './route/types'; import { AppState } from './types'; import { WebSocketAction } from './websocket/types'; jest.mock('../websocket/WebSocketClient'); export const mockStore = (state: AppState) => { const client = new WebSocketClient(''); const middlewares: Middleware[] = [thunk.withExtraArgument(client)]; type DispatchExts = ThunkDispatch; const mockStoreCreator: MockStoreCreator = ( createMockStore(middlewares) ); return { store: mockStoreCreator(state), client }; }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/route/actions.ts ================================================ import { ActionFactory } from '../types'; import { ActionType, AddLocationAction, AddVehicleAction, ClearRouteAction, DeleteLocationAction, DeleteVehicleAction, LatLngWithDescription, RoutingPlan, UpdateRouteAction, } from './types'; export const addVehicle: ActionFactory = () => ({ type: ActionType.ADD_VEHICLE, }); export const deleteVehicle: ActionFactory = (id) => ({ type: ActionType.DELETE_VEHICLE, value: id, }); export const addLocation: ActionFactory = (location) => ({ type: ActionType.ADD_LOCATION, value: location, }); export const deleteLocation: ActionFactory = (id) => ({ type: ActionType.DELETE_LOCATION, value: id, }); export const clearRoute: ActionFactory = () => ({ type: ActionType.CLEAR_SOLUTION, }); export const updateRoute: ActionFactory = (plan) => ({ plan, type: ActionType.UPDATE_ROUTING_PLAN, }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/route/index.ts ================================================ import * as routeOperations from './operations'; import { routeReducer } from './reducers'; import * as routeSelectors from './selectors'; export { routeOperations, routeSelectors }; export default routeReducer; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/route/operations.ts ================================================ import { ThunkCommandFactory } from '../types'; import * as actions from './actions'; import { AddLocationAction, AddVehicleAction, ClearRouteAction, DeleteLocationAction, DeleteVehicleAction, LatLngWithDescription, VehicleCapacity, } from './types'; export const { updateRoute } = actions; export const addLocation: ThunkCommandFactory = ( (location) => (dispatch, _getState, client) => { dispatch(actions.addLocation(location)); client.addLocation(location); }); export const deleteLocation: ThunkCommandFactory = ( (locationId) => (dispatch, _getState, client) => { dispatch(actions.deleteLocation(locationId)); client.deleteLocation(locationId); }); export const addVehicle: ThunkCommandFactory = ( () => (dispatch, _getState, client) => { dispatch(actions.addVehicle()); client.addVehicle(); }); export const deleteVehicle: ThunkCommandFactory = ( (vehicleId) => (dispatch, _getState, client) => { dispatch(actions.deleteVehicle(vehicleId)); client.deleteVehicle(vehicleId); }); export const deleteAnyVehicle: ThunkCommandFactory = ( () => (_dispatch, _getState, client) => { client.deleteAnyVehicle(); }); export const changeVehicleCapacity: ThunkCommandFactory = ( ({ vehicleId, capacity }: VehicleCapacity) => (_dispatch, _getState, client) => { client.changeVehicleCapacity(vehicleId, capacity); }); export const clearRoute: ThunkCommandFactory = ( () => (dispatch, _getState, client) => { dispatch(actions.clearRoute()); client.clear(); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/route/reducers.ts ================================================ import { ActionType, RouteAction, RoutingPlan } from './types'; export const initialRouteState: RoutingPlan = { distance: 'no data', vehicles: [], depot: null, visits: [], routes: [], }; // eslint-disable-next-line @typescript-eslint/default-param-last export const routeReducer = (state = initialRouteState, action: RouteAction): RoutingPlan => { switch (action.type) { case ActionType.UPDATE_ROUTING_PLAN: { return action.plan; } default: return state; } }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/route/route.test.ts ================================================ import { mockStore } from '../mockStore'; import { AppState } from '../types'; import { WebSocketConnectionStatus } from '../websocket/types'; import * as actions from './actions'; import reducer, { routeOperations, routeSelectors } from './index'; import { initialRouteState } from './reducers'; import { LatLngWithDescription, Vehicle, VehicleCapacity } from './types'; describe('Route operations', () => { it('clearRoute() should call client', () => { const { store, client } = mockStore(state); store.dispatch(routeOperations.clearRoute()); expect(store.getActions()).toEqual([actions.clearRoute()]); expect(client.clear).toHaveBeenCalledTimes(1); }); it('deleteLocation() should call client', () => { const { store, client } = mockStore(state); const id = 3214; store.dispatch(routeOperations.deleteLocation(id)); expect(store.getActions()).toEqual([actions.deleteLocation(id)]); expect(client.deleteLocation).toHaveBeenCalledTimes(1); expect(client.deleteLocation).toHaveBeenCalledWith(id); }); it('deleteVehicle() should call client', () => { const { store, client } = mockStore(state); const id = 5; store.dispatch(routeOperations.deleteVehicle(id)); expect(store.getActions()).toEqual([actions.deleteVehicle(id)]); expect(client.deleteVehicle).toHaveBeenCalledTimes(1); expect(client.deleteVehicle).toHaveBeenCalledWith(id); }); it('deleteAnyVehicle() should call client', () => { const { store, client } = mockStore(state); store.dispatch(routeOperations.deleteAnyVehicle()); expect(store.getActions()).toEqual([]); expect(client.deleteAnyVehicle).toHaveBeenCalledTimes(1); }); it('addLocation() should call client', () => { const { store, client } = mockStore(state); const location: LatLngWithDescription = { lat: 11.01, lng: -35.79, description: 'new location' }; store.dispatch(routeOperations.addLocation(location)); expect(store.getActions()).toEqual([actions.addLocation(location)]); expect(client.addLocation).toHaveBeenCalledTimes(1); expect(client.addLocation).toHaveBeenCalledWith(location); }); it('addVehicle() should call client', () => { const { store, client } = mockStore(state); store.dispatch(routeOperations.addVehicle()); expect(store.getActions()).toEqual([actions.addVehicle()]); expect(client.addVehicle).toHaveBeenCalledTimes(1); expect(client.addVehicle).toHaveBeenCalledWith(); }); it('changeVehicleCapacity() should call client', () => { const { store, client } = mockStore(state); const capacityChange: VehicleCapacity = { vehicleId: 5, capacity: 50 }; store.dispatch(routeOperations.changeVehicleCapacity(capacityChange)); expect(store.getActions()).toEqual([]); expect(client.changeVehicleCapacity).toHaveBeenCalledWith(capacityChange.vehicleId, capacityChange.capacity); }); }); describe('Route reducers', () => { it('clear route', () => { expect( reducer(state.plan, actions.clearRoute()), ).toEqual(state.plan); }); it('add location', () => { expect( reducer(state.plan, actions.addLocation({ lat: 1, lng: -1, description: 'description', })), ).toEqual(state.plan); }); it('delete location', () => { expect( reducer(state.plan, actions.deleteLocation(1)), ).toEqual(state.plan); }); it('add vehicle', () => { expect( reducer(state.plan, actions.addVehicle()), ).toEqual(state.plan); }); it('delete vehicle', () => { expect( reducer(state.plan, actions.deleteVehicle(7)), ).toEqual(state.plan); }); it('update route', () => { expect( reducer(initialRouteState, actions.updateRoute(state.plan)), ).toEqual(state.plan); }); }); describe('Route selectors', () => { it('select total capacity', () => { expect( routeSelectors.totalCapacity(state.plan), ).toEqual(vehicle1.capacity + vehicle2.capacity); }); it('select total demand', () => { expect( routeSelectors.totalDemand(state.plan), // Currently the default demand is 1 per visit. ).toEqual(state.plan.visits.length); }); }); const vehicle1: Vehicle = { id: 1, name: 'v1', capacity: 5 }; const vehicle2: Vehicle = { id: 2, name: 'v2', capacity: 5 }; const visit1 = { id: 1, lat: 1.345678, lng: 1.345678, }; const visit2 = { id: 2, lat: 2.345678, lng: 2.345678, }; const visit3 = { id: 3, lat: 3.676111, lng: 3.568333, }; const visit4 = { id: 4, lat: 4.345678, lng: 4.345678, }; const visit5 = { id: 5, lat: 5.345678, lng: 5.345678, }; const state: AppState = { connectionStatus: WebSocketConnectionStatus.CLOSED, messages: [], serverInfo: { boundingBox: null, countryCodes: [], demos: [], }, userViewport: { isDirty: false, zoom: 1, center: [0, 0], }, demo: { demoName: null, isLoading: false, }, plan: { distance: '10', vehicles: [ vehicle1, vehicle2, ], depot: null, visits: [visit1, visit2, visit3, visit4, visit5], routes: [{ vehicle: vehicle1, visits: [visit1, visit2, visit3], track: [[0.111222, 0.222333], [0.444555, 0.555666]], }, { vehicle: vehicle2, visits: [visit4, visit5], track: [[0.41, 0.42], [0.51, 0.52]], }], }, }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/route/selectors.ts ================================================ import { RoutingPlan } from 'store/route/types'; export const totalCapacity = (plan: RoutingPlan) => plan.vehicles .map((vehicle) => vehicle.capacity) .reduce((a, b) => a + b, 0); export const totalDemand = (plan: RoutingPlan) => plan.visits.length; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/route/types.ts ================================================ import { Action } from 'redux'; export interface LatLng { readonly lat: number; readonly lng: number; } export interface LatLngWithDescription extends LatLng { description: string; } export interface Location extends LatLng { readonly id: number; // TODO decide between optional, nullable and more complex structure (displayName, fullDescription, address, ...) readonly description?: string; } export interface Vehicle { readonly id: number; readonly name: string; readonly capacity: number; } export interface Route { readonly vehicle: Vehicle; // TODO change to vehicleId readonly visits: Location[]; } export type LatLngTuple = [number, number]; export interface RouteWithTrack extends Route { readonly track: LatLngTuple[]; } export interface RoutingPlan { readonly distance: string; readonly vehicles: Vehicle[]; readonly depot: Location | null; readonly visits: Location[]; readonly routes: RouteWithTrack[]; } export enum ActionType { UPDATE_ROUTING_PLAN = 'UPDATE_ROUTING_PLAN', DELETE_LOCATION = 'DELETE_LOCATION', ADD_LOCATION = 'ADD_LOCATION', ADD_VEHICLE = 'ADD_VEHICLE', DELETE_VEHICLE = 'DELETE_VEHICLE', CLEAR_SOLUTION = 'CLEAR_SOLUTION', } export interface AddLocationAction extends Action { readonly value: LatLngWithDescription; } export interface AddVehicleAction extends Action { } export interface ClearRouteAction extends Action { } export interface DeleteLocationAction extends Action { readonly value: number; } export interface DeleteVehicleAction extends Action { readonly value: number; } export interface VehicleCapacity { vehicleId: number; capacity: number; } export interface UpdateRouteAction extends Action { readonly plan: RoutingPlan; } export type RouteAction = | AddLocationAction | AddVehicleAction | DeleteLocationAction | DeleteVehicleAction | UpdateRouteAction | ClearRouteAction; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/server/actions.ts ================================================ import { ActionFactory } from '../types'; import { ActionType, ServerInfo, ServerInfoAction } from './types'; export const serverInfo: ActionFactory = (info) => ({ type: ActionType.SERVER_INFO, value: info, }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/server/index.ts ================================================ import * as serverOperations from './operations'; import { routeReducer } from './reducers'; export { serverOperations }; export default routeReducer; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/server/operations.ts ================================================ import { resetViewport } from '../client/actions'; import { ResetViewportAction } from '../client/types'; import { ThunkCommandFactory } from '../types'; import * as actions from './actions'; import { ServerInfo, ServerInfoAction } from './types'; export const serverInfo: ThunkCommandFactory = ( (info) => (dispatch) => { dispatch(resetViewport()); dispatch(actions.serverInfo(info)); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/server/reducers.ts ================================================ import { ActionType, ServerInfo, ServerInfoAction } from './types'; export const initialServerState: ServerInfo = { boundingBox: null, countryCodes: [], demos: [], }; // eslint-disable-next-line @typescript-eslint/default-param-last export const routeReducer = (state = initialServerState, action: ServerInfoAction): ServerInfo => { switch (action.type) { case ActionType.SERVER_INFO: { return action.value; } default: return state; } }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/server/server.test.ts ================================================ import * as actions from './actions'; import reducer from './index'; import { initialServerState } from './reducers'; import { ServerInfo } from './types'; describe('Server reducer', () => { const serverInfo: ServerInfo = { boundingBox: null, countryCodes: ['CZ', 'SK'], demos: [{ name: 'Demo name', visits: 10 }], }; it('server info', () => { expect( reducer(initialServerState, actions.serverInfo(serverInfo)), ).toEqual(serverInfo); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/server/types.ts ================================================ import { Action } from 'redux'; import { LatLng } from '../route/types'; export enum ActionType { SERVER_INFO = 'SERVER_INFO', } export interface ServerInfoAction extends Action { value: ServerInfo; } export interface Demo { name: string; visits: number; } export type BoundingBox = [LatLng, LatLng]; export interface ServerInfo { boundingBox: BoundingBox | null; countryCodes: string[]; demos: Demo[]; } ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/store.ts ================================================ import { applyMiddleware, combineReducers, createStore, Store } from 'redux'; // it's possible to disable the extension in production // by importing from redux-devtools-extension/developmentOnly import { composeWithDevTools } from 'redux-devtools-extension'; import { createLogger } from 'redux-logger'; import thunk from 'redux-thunk'; import WebSocketClient from 'websocket/WebSocketClient'; import clientReducer from './client'; import demoReducer from './demo'; import messageReducer from './message'; import routeReducer from './route'; import serverInfoReducer from './server'; import { AppState } from './types'; import connectionReducer from './websocket'; export interface StoreConfig { readonly backendUrl: string; } export function configureStore( { backendUrl }: StoreConfig, preloadedState?: AppState, ): Store { const webSocketClient = new WebSocketClient(backendUrl); const middlewares = [thunk.withExtraArgument(webSocketClient), createLogger()]; const middlewareEnhancer = applyMiddleware(...middlewares); const enhancers = [middlewareEnhancer]; const composedEnhancers = composeWithDevTools(...enhancers); // map reducers to state slices const rootReducer = combineReducers({ connectionStatus: connectionReducer, messages: messageReducer, serverInfo: serverInfoReducer, demo: demoReducer, plan: routeReducer, userViewport: clientReducer, }); return createStore( rootReducer, preloadedState, composedEnhancers, ); } ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/types.ts ================================================ import { Action } from 'redux'; import { ThunkAction } from 'redux-thunk'; import { Message } from 'store/message/types'; import WebSocketClient from 'websocket/WebSocketClient'; import { UserViewport } from './client/types'; import { Demo } from './demo/types'; import { RoutingPlan } from './route/types'; import { ServerInfo } from './server/types'; import { WebSocketConnectionStatus } from './websocket/types'; /** * ThunkCommand is a ThunkAction that has no result (it's typically something like * `Promise`, but sending messages over WebSocket usually has no response * (with the exception of subscribe), so most of our operations are void). * * @template A Type of action(s) allowed to be dispatched. */ export type ThunkCommand = ThunkAction; /** * Factory method that takes a value and creates an @type {Action}. * * @template V value type * @template A action type */ export type ActionFactory = V extends void ? // https://stackoverflow.com/questions/55646272/conditional-method-parameters-based-on-generic-type () => A : // nullary (value: V) => A; // unary /** * Factory method that takes a value and creates a @type {ThunkCommand}. * * @template V value type * @template A action type */ export type ThunkCommandFactory = V extends void ? () => ThunkCommand : // nullary (value: V) => ThunkCommand; // unary export interface AppState { readonly serverInfo: ServerInfo; readonly messages: Message[]; readonly plan: RoutingPlan; readonly connectionStatus: WebSocketConnectionStatus; readonly demo: Demo; readonly userViewport: UserViewport; } ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/websocket/actions.ts ================================================ import { ActionFactory } from '../types'; import { ActionType, InitWsConnectionAction, WsConnectionFailureAction, WsConnectionSuccessAction } from './types'; export const initWsConnection: ActionFactory = () => ({ type: ActionType.WS_CONNECT, }); export const wsConnectionSuccess: ActionFactory = () => ({ type: ActionType.WS_CONNECT_SUCCESS, }); export const wsConnectionFailure: ActionFactory = (error) => ({ type: ActionType.WS_CONNECT_FAILURE, error, }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/websocket/index.ts ================================================ import * as websocketOperations from './operations'; import { wsReducer } from './reducers'; export { websocketOperations }; export default wsReducer; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/websocket/operations.ts ================================================ import { demoOperations } from '../demo'; import { FinishLoadingAction } from '../demo/types'; import { messageActions } from '../message'; import { MessageAction } from '../message/types'; import { routeOperations } from '../route'; import { UpdateRouteAction } from '../route/types'; import { serverOperations } from '../server'; import { ServerInfoAction } from '../server/types'; import { ThunkCommandFactory } from '../types'; import * as actions from './actions'; import { WebSocketAction } from './types'; type ConnectClientThunkAction = | WebSocketAction | MessageAction | UpdateRouteAction | FinishLoadingAction | ServerInfoAction; /** * Connect the client to WebSocket. */ export const connectClient: ThunkCommandFactory = ( () => (dispatch, getState, client) => { // Dispatch the WS connection initializing event. dispatch(actions.initWsConnection()); client.connect( // On connection, subscribe to the route topic. () => { dispatch(actions.wsConnectionSuccess()); client.subscribeToServerInfo((serverInfo) => { dispatch(serverOperations.serverInfo(serverInfo)); }); client.subscribeToErrorTopic((errorMessage) => { dispatch(messageActions.receiveMessage(errorMessage)); }); client.subscribeToRoute((plan) => { dispatch(routeOperations.updateRoute(plan)); if (getState().demo.isLoading) { // TODO handle the case when serverInfo doesn't contain demo with the given name // (that could only be possible due to a bug in the code) const demo = getState().serverInfo.demos.filter((value) => value.name === getState().demo.demoName)[0]; if (plan.visits.length === demo.visits) { dispatch(demoOperations.finishLoading()); } } }); }, // On error, schedule a one-time reconnection attempt. (err) => { // TODO try to pass the original err object or test it here and // dispatch different actions based on its properties (Frame vs. CloseEvent, reason etc.) dispatch(actions.wsConnectionFailure(JSON.stringify(err))); setTimeout(() => dispatch(connectClient()), 1000); }, ); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/websocket/reducers.ts ================================================ import { ActionType, WebSocketAction, WebSocketConnectionStatus } from './types'; export const wsReducer = ( // eslint-disable-next-line @typescript-eslint/default-param-last (state = WebSocketConnectionStatus.CLOSED, action: WebSocketAction): WebSocketConnectionStatus => { switch (action.type) { case ActionType.WS_CONNECT_SUCCESS: { return WebSocketConnectionStatus.OPEN; } case ActionType.WS_CONNECT_FAILURE: { return WebSocketConnectionStatus.ERROR; } default: return state; } }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/websocket/types.ts ================================================ import { Action } from 'redux'; export enum WebSocketConnectionStatus { OPEN = 'OPEN', CLOSED = 'CLOSED', ERROR = 'ERROR', } export enum ActionType { WS_CONNECT = 'WS_CONNECT', WS_CONNECT_SUCCESS = 'WS_CONNECT_SUCCESS', WS_CONNECT_FAILURE = 'WS_CONNECT_FAILURE', } export interface InitWsConnectionAction extends Action { } export interface WsConnectionSuccessAction extends Action { } export interface WsConnectionFailureAction extends Action { readonly error: string; } export type WebSocketAction = | InitWsConnectionAction | WsConnectionFailureAction | WsConnectionSuccessAction; ================================================ FILE: optaweb-vehicle-routing-frontend/src/store/websocket/websocket.test.ts ================================================ import { MessagePayload } from 'store/message/types'; import { resetViewport } from '../client/actions'; import { UserViewport } from '../client/types'; import { demoOperations } from '../demo'; import { receiveMessage } from '../message/actions'; import { mockStore } from '../mockStore'; import { routeOperations } from '../route'; import { RoutingPlan, Vehicle } from '../route/types'; import { serverInfo } from '../server/actions'; import { ServerInfo } from '../server/types'; import { AppState } from '../types'; import * as actions from './actions'; import reducer, { websocketOperations } from './index'; import { WebSocketConnectionStatus } from './types'; const uninitializedCallbackCapture = () => { throw new Error('Error callback is uninitialized'); }; const userViewport: UserViewport = { isDirty: true, zoom: 1, center: [0, 0], }; beforeEach(() => { jest.useFakeTimers(); jest.spyOn(global, 'setTimeout'); }); describe('WebSocket client operations', () => { it('should fail connection and reconnect when client crashes', () => { let errorCallbackCapture: (err: any) => void = uninitializedCallbackCapture; let successCallbackCapture: () => void = uninitializedCallbackCapture; let subscribeCallbackCapture: (plan: RoutingPlan) => void = uninitializedCallbackCapture; const { store, client } = mockStore(state); client.connect = jest.fn().mockImplementation((successCallback, errorCallback) => { successCallbackCapture = successCallback; errorCallbackCapture = errorCallback; }); client.subscribeToRoute = jest.fn().mockImplementation((callback) => { subscribeCallbackCapture = callback; }); store.dispatch(websocketOperations.connectClient()); expect(store.getActions()).toEqual([actions.initWsConnection()]); // simulate client disconnection const testError = { error: 'TEST_ERROR' }; errorCallbackCapture(testError); expect(store.getActions()).toEqual([ actions.initWsConnection(), actions.wsConnectionFailure(JSON.stringify(testError)), ]); store.clearActions(); // verify reconnection has been scheduled expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); jest.runOnlyPendingTimers(); expect(store.getActions()).toEqual([actions.initWsConnection()]); // pretend client will reconnect successfully on the next attempt successCallbackCapture(); expect(store.getActions()).toEqual([ actions.initWsConnection(), actions.wsConnectionSuccess(), ]); expect(client.subscribeToRoute).toHaveBeenCalledTimes(1); store.clearActions(); // simulate response to subscription subscribeCallbackCapture(emptyPlan); expect(store.getActions()).toEqual([routeOperations.updateRoute(emptyPlan)]); }); it('should finish demo loading when all locations are loaded', () => { const stateWithDemo: AppState = { ...state, serverInfo: { boundingBox: null, countryCodes: [], demos: [{ name: 'demo', visits: nonEmptyPlan.visits.length, }], }, demo: { demoName: 'demo', isLoading: true, }, }; const { store, client } = mockStore(stateWithDemo); let successCallbackCapture: () => void = uninitializedCallbackCapture; client.connect = jest.fn().mockImplementation((successCallback) => { successCallbackCapture = successCallback; }); let routeSubscriptionCallback: (plan: RoutingPlan) => void = uninitializedCallbackCapture; client.subscribeToRoute = jest.fn().mockImplementation((callback) => { routeSubscriptionCallback = callback; }); // connect the client store.dispatch(websocketOperations.connectClient()); expect(store.getActions()).toEqual([actions.initWsConnection()]); // simulate successful client connection successCallbackCapture(); expect(store.getActions()).toEqual([ actions.initWsConnection(), actions.wsConnectionSuccess(), ]); // should be subscribed to all topics expect(client.subscribeToRoute).toHaveBeenCalledTimes(1); store.clearActions(); // simulate receiving plan with number of visits matching the expected demo size routeSubscriptionCallback(nonEmptyPlan); // FINISH_LOADING should be dispatched expect(store.getActions()).toEqual([ routeOperations.updateRoute(nonEmptyPlan), demoOperations.finishLoading(), ]); }); it('should dispatch server info and reset viewport', () => { const { store, client } = mockStore(state); let successCallbackCapture: () => void = uninitializedCallbackCapture; client.connect = jest.fn().mockImplementation((successCallback) => { successCallbackCapture = successCallback; }); let serverInfoSubscriptionCallback: (info: ServerInfo) => void = uninitializedCallbackCapture; client.subscribeToServerInfo = jest.fn().mockImplementation((callback) => { serverInfoSubscriptionCallback = callback; }); // successfully connect the client store.dispatch(websocketOperations.connectClient()); successCallbackCapture(); // should be subscribed serverInfo topic expect(client.subscribeToServerInfo).toHaveBeenCalledTimes(1); store.clearActions(); // when server info arrives const info: ServerInfo = { boundingBox: null, countryCodes: ['AB', 'XY'], demos: [{ name: 'Demo name', visits: 20 }], }; serverInfoSubscriptionCallback(info); // action should be dispatched expect(store.getActions()).toEqual([ resetViewport(), serverInfo(info), ]); }); it('should dispatch errors', () => { const { store, client } = mockStore(state); let successCallbackCapture: () => void = uninitializedCallbackCapture; client.connect = jest.fn().mockImplementation((successCallback) => { successCallbackCapture = successCallback; }); let errorTopicSubscriptionCallback: (message: MessagePayload) => void = uninitializedCallbackCapture; client.subscribeToErrorTopic = jest.fn().mockImplementation((callback) => { errorTopicSubscriptionCallback = callback; }); // successfully connect the client store.dispatch(websocketOperations.connectClient()); successCallbackCapture(); // should be subscribed error topic expect(client.subscribeToErrorTopic).toHaveBeenCalledTimes(1); store.clearActions(); // when error message arrives const message: MessagePayload = { id: '1', text: '2' }; errorTopicSubscriptionCallback(message); // action should be dispatched expect(store.getActions()).toEqual([ receiveMessage(message), ]); }); }); describe('WebSocket reducers', () => { it('connection success should open connection status', () => { expect( reducer(WebSocketConnectionStatus.CLOSED, actions.wsConnectionSuccess()), ).toEqual(WebSocketConnectionStatus.OPEN); }); it('connection failure should fail connection status', () => { expect( reducer(WebSocketConnectionStatus.OPEN, actions.wsConnectionFailure('test error')), ).toEqual(WebSocketConnectionStatus.ERROR); }); }); const emptyPlan: RoutingPlan = { distance: '', vehicles: [], depot: null, visits: [], routes: [], }; const vehicle1: Vehicle = { id: 1, name: 'v1', capacity: 5 }; const vehicle2: Vehicle = { id: 2, name: 'v2', capacity: 5 }; const visit1 = { id: 1, lat: 1.345678, lng: 1.345678, }; const visit2 = { id: 2, lat: 2.345678, lng: 2.345678, }; const visit3 = { id: 3, lat: 3.676111, lng: 3.568333, }; const visit4 = { id: 4, lat: 4.345678, lng: 4.345678, }; const visit5 = { id: 5, lat: 5.345678, lng: 5.345678, }; const visit6 = { id: 6, lat: 6.676111, lng: 6.568333, }; const nonEmptyPlan: RoutingPlan = { distance: '1.0', vehicles: [ vehicle1, vehicle2, ], depot: visit1, visits: [visit2, visit3, visit4, visit5, visit6], routes: [], // not important for the test }; const state: AppState = { connectionStatus: WebSocketConnectionStatus.CLOSED, messages: [], serverInfo: { boundingBox: null, countryCodes: [], demos: [], }, demo: { demoName: null, isLoading: false, }, plan: emptyPlan, userViewport, }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/App.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import App from './App'; describe('App', () => { it('should render correctly', () => { const app = shallow(); expect(toJson(app)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/App.tsx ================================================ import { Page, PageSection } from '@patternfly/react-core'; import * as React from 'react'; import { Route, Switch } from 'react-router-dom'; import Alerts from 'ui/components/Alerts'; import Background from './components/Background'; import { ConnectionManager } from './connection'; import Header from './header/Header'; import { Demo, Route as RoutePage, Vehicles, Visits } from './pages'; export const pagesByPath = [ { path: { canonical: '/demo', aliases: ['/'] }, page: Demo, label: 'Demo' }, { path: { canonical: '/vehicles', aliases: [] }, page: Vehicles, label: 'Vehicles' }, { path: { canonical: '/visits', aliases: [] }, page: Visits, label: 'Visits' }, { path: { canonical: '/routes', aliases: [] }, page: RoutePage, label: 'Routes' }, ]; const App: React.FC = () => ( <> }> {pagesByPath.map(({ path, page }) => ([path.canonical, ...path.aliases].map((p) => ( ))))} ); export default App; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/__snapshots__/App.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`App should render correctly 1`] = ` } isBreadcrumbWidthLimited={false} isManagedSidebar={false} isNotificationDrawerExpanded={false} mainTabIndex={-1} onNotificationDrawerExpand={[Function]} onPageResize={[Function]} > `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/Alerts.test.tsx ================================================ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { Alerts, Props } from 'ui/components/Alerts'; import { shallow, toJson } from 'ui/shallow-test-util'; describe('Alerts', () => { it('should call readMessage() when alert is closed', async () => { const props: Props = { messages: [ { id: '1', text: 'msg 1', status: 'new' }, { id: '2', text: 'msg 2', status: 'new' }, ], readMessage: jest.fn(), }; const user = userEvent.setup(); // TODO add a shallow render test const alerts = render(); expect(alerts).toMatchSnapshot(); await user.click(screen.getAllByTitle('Close alert')[1]); expect(props.readMessage).toHaveBeenCalledWith('2'); }); it('should not render if there are no messages', () => { const props: Props = { messages: [], readMessage: jest.fn(), }; const alerts = shallow(); expect(toJson(alerts)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/Alerts.tsx ================================================ import { Alert, AlertActionCloseButton, AlertGroup } from '@patternfly/react-core'; import * as React from 'react'; import { connect } from 'react-redux'; import { messageActions, messageSelectors } from 'store/message'; import { Message } from 'store/message/types'; import { AppState } from 'store/types'; interface StateProps { messages: Message[]; } const mapStateToProps = ({ messages }: AppState): StateProps => ({ messages: messageSelectors.getNewMessages(messages), }); interface DispatchProps { readMessage: typeof messageActions.readMessage; } const mapDispatchToProps: DispatchProps = { readMessage: messageActions.readMessage, }; export type Props = StateProps & DispatchProps; export const Alerts: React.FC = ({ messages, readMessage }: Props) => ( messages.length > 0 ? ( {messages .map(({ id, text }) => ( readMessage(id)} /> )} > {text} ))} ) : null ); export default connect(mapStateToProps, mapDispatchToProps)(Alerts); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/Background.tsx ================================================ import { BackgroundImage } from '@patternfly/react-core'; import pfBackground1200 from '@patternfly/react-core/dist/styles/assets/images/pfbg_1200.jpg'; import pfBackground576 from '@patternfly/react-core/dist/styles/assets/images/pfbg_576.jpg'; import pfBackground1152 from '@patternfly/react-core/dist/styles/assets/images/pfbg_576@2x.jpg'; import pfBackground768 from '@patternfly/react-core/dist/styles/assets/images/pfbg_768.jpg'; import pfBackground1536 from '@patternfly/react-core/dist/styles/assets/images/pfbg_768@2x.jpg'; import * as React from 'react'; const images = { xs: pfBackground576, sm: pfBackground768, xs2x: pfBackground1152, lg: pfBackground1200, sm2x: pfBackground1536, }; const Background: React.FC = () => ; export default Background; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/DemoDropdown.css ================================================ /* Override dropdown menu z-index to be higher than Leaflet map's z-index */ .pf-c-dropdown__menu { --pf-c-dropdown__menu--ZIndex: 2000; } ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/DemoDropdown.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import { DemoDropdown, Props } from './DemoDropdown'; describe('Demo dropdown button', () => { it('should render correctly with a couple of demos', () => { const props: Props = { demos: ['demo 1', 'demo 2'], onSelect: jest.fn(), }; const dropdown = shallow(); expect(toJson(dropdown)).toMatchSnapshot(); }); it('should be disabled with empty demos', () => { const props: Props = { demos: [], onSelect: jest.fn(), }; const dropdown = shallow(); expect(toJson(dropdown)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/DemoDropdown.tsx ================================================ import { Dropdown, DropdownItem, DropdownPosition, DropdownToggle } from '@patternfly/react-core'; import * as React from 'react'; import './DemoDropdown.css'; export interface Props { demos: string[]; onSelect: (name: string) => void; } const dropdownItems = (demos: string[]): React.ReactNode[] => demos.map((value) => ( {value} )); export const DemoDropdown: React.FC = ({ demos, onSelect }) => { const [isOpen, setOpen] = React.useState(false); return ( { setOpen(false); if (e && e.currentTarget) { onSelect(e.currentTarget.innerText); } }} toggle={( setOpen(!isOpen)} > Load demo )} /> ); }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/Location.test.tsx ================================================ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { shallow, toJson } from 'ui/shallow-test-util'; import Location, { LocationProps } from './Location'; describe('Location Component', () => { it('should call handlers', async () => { const props: LocationProps = { id: 10, description: 'x', removeDisabled: false, removeHandler: jest.fn(), selectHandler: jest.fn(), }; render(); const user = userEvent.setup(); await user.click(screen.getByRole('button')); expect(props.removeHandler).toHaveBeenCalledTimes(1); expect(props.selectHandler).toHaveBeenCalledTimes(1); }); it('should render correctly', () => { const props: LocationProps = { id: 10, description: 'x', removeDisabled: false, removeHandler: jest.fn(), selectHandler: jest.fn(), }; const location = shallow(); expect(toJson(location)).toMatchSnapshot(); }); it('should render correctly when description is missing', () => { const props: LocationProps = { id: 11, description: null, removeDisabled: false, removeHandler: jest.fn(), selectHandler: jest.fn(), }; const location = shallow(); expect(toJson(location)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/Location.tsx ================================================ import { Button, DataListCell, DataListItem, DataListItemRow, Tooltip } from '@patternfly/react-core'; import { TimesIcon } from '@patternfly/react-icons'; import * as React from 'react'; export interface LocationProps { id: number; description: string | null; removeDisabled: boolean; removeHandler: (id: number) => void; selectHandler: (id: number) => void; } const Location: React.FC = ({ id, description, removeDisabled, removeHandler, selectHandler, }) => { const [clicked, setClicked] = React.useState(false); function shorten(text: string) { const first = text.replace(/,.*/, '').trim(); const short = first.substring(0, Math.min(20, first.length)).trim(); if (short.length < first.length) { return `${short}...`; } return short; } return ( selectHandler(id)} onMouseLeave={() => selectHandler(NaN)} > {(description && ( {shorten(description)} )) || {`Location ${id}`}} ); }; export default Location; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/LocationList.css ================================================ .pf-c-data-list__cell { --pf-c-data-list__cell-cell--PaddingTop: var(--pf-global--spacer--sm); --pf-c-data-list__cell--PaddingTop: var(--pf-global--spacer--md); --pf-c-data-list__cell--PaddingBottom: var(--pf-global--spacer--sm); } ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/LocationList.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import LocationList, { LocationListProps } from './LocationList'; describe('Location List Component', () => { it('should render correctly with no routes', () => { const props: LocationListProps = { removeHandler: jest.fn(), selectHandler: jest.fn(), depot: null, visits: [], }; const locationList = shallow(); expect(toJson(locationList)).toMatchSnapshot(); }); it('should render correctly with a few routes', () => { const props: LocationListProps = { removeHandler: jest.fn(), selectHandler: jest.fn(), depot: { id: 1, lat: 1.345678, lng: 1.345678, description: 'Depot', }, visits: [ { id: 2, lat: 2.345678, lng: 2.345678, description: 'Visit 1', }, { id: 3, lat: 3.676111, lng: 3.568333, description: 'Visit 2', }, ], }; const locationList = shallow(); expect(toJson(locationList)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/LocationList.tsx ================================================ import { Bullseye, DataList } from '@patternfly/react-core'; import * as React from 'react'; import { Location } from 'store/route/types'; import LocationItem from './Location'; import './LocationList.css'; export interface LocationListProps { removeHandler: (id: number) => void; selectHandler: (id: number) => void; depot: Location | null; visits: Location[]; } const renderEmptyLocationList: React.FC = () => ( No locations ); const renderLocationList: React.FC = ({ depot, visits, removeHandler, selectHandler, }) => (
{depot && ( 0} removeHandler={removeHandler} selectHandler={selectHandler} /> )} {visits .slice(0) // clone the array because // sort is done in place (that would affect the route) .sort((a, b) => a.id - b.id) .map((visit) => ( ))}
); const LocationList: React.FC = (props) => ( props.visits.length === 0 && props.depot === null ? renderEmptyLocationList(props) : renderLocationList(props) ); export default LocationList; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/LocationMarker.test.tsx ================================================ import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Location } from 'store/route/types'; import { shallow, toJson } from 'ui/shallow-test-util'; import LocationMarker, { Props } from './LocationMarker'; const location: Location = { id: 1, lat: 1.345678, lng: 1.345678, }; describe('Location Marker', () => { it('render depot', () => { const props: Props = { removeHandler: jest.fn(), isDepot: true, isSelected: false, location, }; const locationMarker = shallow(); expect(toJson(locationMarker)).toMatchSnapshot(); }); it('render visit', () => { const props: Props = { removeHandler: jest.fn(), isDepot: false, isSelected: false, location, }; const locationMarker = shallow(); expect(toJson(locationMarker)).toMatchSnapshot(); }); it('selected visit should show a tooltip', () => { const props: Props = { removeHandler: jest.fn(), isDepot: false, isSelected: true, location, }; const locationMarker = shallow(); expect(toJson(locationMarker)).toMatchSnapshot(); }); xit('should call remove handler when clicked', async () => { const props: Props = { removeHandler: jest.fn(), isDepot: false, isSelected: true, location, }; // FIXME Cannot read property 'addLayer' of undefined render(); userEvent.setup(); // TODO // await user.click(screen.find(Marker)); expect(props.removeHandler).toBeCalled(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/LocationMarker.tsx ================================================ import * as L from 'leaflet'; import * as React from 'react'; import { Marker, Tooltip } from 'react-leaflet'; import { Location } from 'store/route/types'; const homeIcon = L.icon({ iconAnchor: [12, 12], iconSize: [24, 24], iconUrl: 'if_big_house-home_2222740.png', popupAnchor: [0, -10], shadowAnchor: [16, 2], shadowSize: [50, 16], shadowUrl: 'if_big_house-home_2222740_shadow.png', }); const defaultIcon = new L.Icon.Default(); export interface Props { location: Location; isDepot: boolean; isSelected: boolean; removeHandler: (id: number) => void; } const LocationMarker: React.FC = ({ location, isDepot, isSelected, removeHandler, }) => { const icon = isDepot ? homeIcon : defaultIcon; return ( removeHandler(location.id)} > {`Location ${location.id} [Lat=${location.lat}, Lng=${location.lng}]`} ); }; export default LocationMarker; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/RouteMap.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import RouteMap, { Props } from './RouteMap'; describe('Route Map', () => { it('should show the whole world when bounding box is null', () => { const props: Props = { updateViewport: jest.fn, clickHandler: jest.fn(), removeHandler: jest.fn(), selectedId: 1, depot: { id: 1, lat: 1.345678, lng: 1.345678, }, visits: [], routes: [{ visits: [], track: [], }], boundingBox: null, userViewport: { isDirty: false, zoom: 4, center: [1, 1], }, }; const routeMap = shallow(); expect(toJson(routeMap)).toMatchSnapshot(); }); it('should pan and zoom to show bounding box if viewport is not dirty', () => { const depot = { id: 1, lat: 1.345678, lng: 1.345678, }; const visit2 = { id: 2, lat: 2.345678, lng: 2.345678, }; const visit3 = { id: 3, lat: 3.676111, lng: 3.568333, }; const props: Props = { updateViewport: jest.fn(), clickHandler: jest.fn(), removeHandler: jest.fn(), selectedId: 1, boundingBox: [{ lat: -1, lng: -2 }, { lat: 10, lng: 20 }], userViewport: { isDirty: false, zoom: 4, center: [1, 1], }, depot, visits: [visit2, visit3], routes: [{ visits: [visit2, visit3], track: [[0.111222, 0.222333], [0.444555, 0.555666]], }], }; const routeMap = shallow(); expect(toJson(routeMap)).toMatchSnapshot(); }); it('should ignore bounds if viewport is dirty', () => { const depot = { id: 1, lat: 1.345678, lng: 1.345678, }; const props: Props = { updateViewport: jest.fn(), clickHandler: jest.fn(), removeHandler: jest.fn(), selectedId: NaN, boundingBox: [{ lat: -1, lng: -2 }, { lat: 10, lng: 20 }], userViewport: { isDirty: true, zoom: 4, center: [1, 1], }, depot, visits: [], routes: [], }; const routeMap = shallow(); // Map's bounds should be undefined expect(toJson(routeMap)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/RouteMap.tsx ================================================ import * as L from 'leaflet'; import * as React from 'react'; import { Map, Polyline, Rectangle, TileLayer, ZoomControl } from 'react-leaflet'; import { UserViewport } from 'store/client/types'; import { Location, RouteWithTrack } from 'store/route/types'; import { BoundingBox } from 'store/server/types'; import LocationMarker from './LocationMarker'; type Omit = Pick>; export interface Props { selectedId: number; clickHandler: (event: L.LeafletMouseEvent) => void; removeHandler: (id: number) => void; depot: Location | null; visits: Location[]; routes: Omit[]; boundingBox: BoundingBox | null; userViewport: UserViewport; updateViewport: (viewport: UserViewport) => void; } // TODO unlimited unique (random) colors const colors = ['deepskyblue', 'crimson', 'seagreen', 'slateblue', 'gold', 'darkorange']; function color(index: number) { return colors[index % colors.length]; } const RouteMap: React.FC = ({ boundingBox, userViewport, selectedId, depot, visits, routes, clickHandler, removeHandler, updateViewport, }) => { const bounds = boundingBox ? new L.LatLngBounds(boundingBox[0], boundingBox[1]) : undefined; // do not use bounds if user's viewport is dirty const mapBounds = userViewport.isDirty ? undefined : bounds; // TODO make TileLayer URL configurable // @ts-expect-error Cypress exists on window during Cypress test runs const tileLayerUrl = window.Cypress ? 'test-mode-empty-url' : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; return ( {depot && ( )} {visits.map((location) => ( ))} {routes.map((route, index) => ( ))} {bounds && ( )} ); }; export default RouteMap; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/SearchBox.test.tsx ================================================ import { render } from '@testing-library/react'; import { OpenStreetMapProvider } from 'leaflet-geosearch'; import { RawResult } from 'leaflet-geosearch/lib/providers/openStreetMapProvider'; import { SearchResult } from 'leaflet-geosearch/lib/providers/provider'; import { shallow, toJson } from 'ui/shallow-test-util'; import SearchBox, { Props, Result } from './SearchBox'; jest.mock('leaflet-geosearch'); beforeEach(() => { // Clear all instances and calls to constructor and all methods: (OpenStreetMapProvider as unknown as jest.MockInstance).mockClear(); jest.useFakeTimers(); }); const searchProviderMock = ( () => (OpenStreetMapProvider as unknown as jest.MockInstance).mock ); describe('Search box', () => { it('should render text input initially', () => { const props: Props = { addHandler: jest.fn(), boundingBox: null, countryCodeSearchFilter: ['XY'], searchDelay: 1, }; const searchBox = shallow(); expect(toJson(searchBox)).toMatchSnapshot(); }); it('should show results when query is entered', async () => { const props: Props = { addHandler: jest.fn(), boundingBox: null, countryCodeSearchFilter: ['XY'], searchDelay: 1, }; // const user = userEvent.setup(); render(); expect(searchProviderMock().instances).toHaveLength(1); // FIXME user.type() times out /* // text input change triggers a component update await user.type(screen.getByLabelText('geosearch text input'), 'London'); // which in turn creates a new searchProvider instance expect(searchProviderMock().instances).toHaveLength(2); // so we can't provide the mock implementation earlier than here searchProviderMock().instances[1].search = jest.fn().mockImplementation(() => searchResults); await jest.runAllTimers(); expect(searchProviderMock().instances[1].search).toHaveBeenCalledTimes(1); */ // FIXME test state // expect((searchBox.state() as State).results).toHaveLength(searchResults.length); // expect((searchBox.state() as State).results[0].id).toEqual(searchResults[0].raw.place_id); // expect((searchBox.state() as State).attributions).toEqual(licenses); // expect(toJson(searchBox)).toMatchSnapshot(); }); it('should hide results when query is empty', () => { const props: Props = { addHandler: jest.fn(), boundingBox: null, countryCodeSearchFilter: ['XY'], searchDelay: 1, }; shallow(); expect(searchProviderMock().instances).toHaveLength(1); // FIXME test state /* // when there are non-empty results searchBox.setState({ results: stateResults, attributions: licenses }); expect(searchProviderMock().instances).toHaveLength(2); expect((searchBox.state() as State).results).toEqual(stateResults); expect((searchBox.state() as State).attributions).toEqual(licenses); // and an empty query is issued const emptyQuery = ' '; searchBox.find(SearchInput).simulate('change', emptyQuery); expect(searchProviderMock().instances).toHaveLength(3); expect(toJson(searchBox)).toMatchSnapshot(); // search is not invoked expect(searchProviderMock().instances[2].search).toHaveBeenCalledTimes(0); // and results are cleared expect(searchBox.state()).toEqual({ query: emptyQuery, results: [], attributions: [] }); */ }); it('should invoke add handler with the selected result and clear results', () => { const mockAddHandler = jest.fn(); const props: Props = { addHandler: mockAddHandler, boundingBox: null, countryCodeSearchFilter: ['XY'], searchDelay: 1, }; shallow(); /* // when there are non-empty results searchBox.setState({ results: stateResults, attributions: licenses }); expect(toJson(searchBox)).toMatchSnapshot(); const resultItems = searchBox.findWhere( (node) => node.key() !== null && node.key().startsWith('place-id-'), ); expect(resultItems).toHaveLength(stateResults.length); const selection = Math.floor(stateResults.length / 2); resultItems.at(selection).find(Button).simulate('click'); expect(props.addHandler).toHaveBeenLastCalledWith(stateResults[selection]); expect(searchBox.state()).toEqual({ query: '', results: [], attributions: [] }); */ }); }); const licenses = ['License 1', 'License 2']; // FIXME // eslint-disable-next-line @typescript-eslint/no-unused-vars const searchResults: SearchResult[] = [{ label: 'London, ON, Canada', x: 101, y: 102, bounds: [[1, 2], [3, 4]], // @ts-expect-error discrepancy between leaflet-geosearch API (expects license) and the actual Nominatim data raw: { place_id: 'raw-place-id-1', licence: licenses[0] }, }, { label: 'London, OH, USA', x: 201, y: 202, bounds: [[1, 2], [3, 4]], // @ts-expect-error discrepancy between leaflet-geosearch API (expects license) and the actual Nominatim data raw: { place_id: 'raw-place-id-2', licence: licenses[1] }, }, { label: 'London, KY, USA', x: 301, y: 302, bounds: [[1, 2], [3, 4]], // @ts-expect-error discrepancy between leaflet-geosearch API (expects license) and the actual Nominatim data raw: { place_id: 'raw-place-id-3', licence: licenses[1] }, }, { label: 'London, UK', x: 401, y: 402, bounds: [[1, 2], [3, 4]], // @ts-expect-error discrepancy between leaflet-geosearch API (expects license) and the actual Nominatim data raw: { place_id: 'raw-place-id-4', licence: licenses[0] }, }]; // FIXME // eslint-disable-next-line @typescript-eslint/no-unused-vars const stateResults: Result[] = [{ id: 'place-id-1A', address: 'Address 1', latLng: { lat: 11, lng: 12 }, }, { id: 'place-id-2B', address: 'Address 2', latLng: { lat: 21, lng: 22 }, }, { id: 'place-id-3C', address: 'Address 3', latLng: { lat: 31, lng: 32 }, }, { id: 'place-id-4D', address: 'Address 4', latLng: { lat: 41, lng: 42 }, }, { id: 'place-id-5E', address: 'Address 5', latLng: { lat: 51, lng: 52 }, }]; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/SearchBox.tsx ================================================ import '@patternfly/patternfly/patternfly.css'; import { Button, SearchInput, Text, TextContent, TextVariants } from '@patternfly/react-core'; import { PlusSquareIcon } from '@patternfly/react-icons'; import { OpenStreetMapProvider } from 'leaflet-geosearch'; import { OpenStreetMapProviderOptions } from 'leaflet-geosearch/lib/providers/openStreetMapProvider'; import * as React from 'react'; import { LatLng } from 'store/route/types'; import { BoundingBox } from 'store/server/types'; export interface Result { id: string; address: string; latLng: LatLng; } export interface Props { searchDelay: number; boundingBox: BoundingBox | null; countryCodeSearchFilter: string[]; addHandler: (result: Result) => void; } export interface State { query: string; results: Result[]; attributions: string[]; } // Nominatim API: viewbox=,,, (x is longitude, y is latitude). type ViewBox = [number, number, number, number]; const viewBox: (bb: BoundingBox) => ViewBox = (bb: BoundingBox) => [bb[0].lng, bb[0].lat, bb[1].lng, bb[1].lat]; const providerOptions = (props: Props): OpenStreetMapProviderOptions => ({ params: { countrycodes: props.countryCodeSearchFilter.toString(), viewbox: props.boundingBox ? viewBox(props.boundingBox).toString() : '', bounded: !!props.boundingBox, }, }); class SearchBox extends React.Component { // eslint-disable-next-line max-len // https://github.com/airbnb/javascript/blob/eslint-config-airbnb-v18.1.0/packages/eslint-config-airbnb/rules/react.js#L489 // TODO remove this suppression once the TODO above is resolved: // eslint-disable-next-line react/static-property-placement static defaultProps: Pick = { searchDelay: 500, }; private searchProvider: OpenStreetMapProvider; private timeoutId: number | null; constructor(props: Props) { super(props); this.state = { query: '', results: [], attributions: [], }; this.searchProvider = new OpenStreetMapProvider(providerOptions(props)); this.timeoutId = null; this.handleTextInputChange = this.handleTextInputChange.bind(this); this.handleClick = this.handleClick.bind(this); } componentDidUpdate() { this.searchProvider = new OpenStreetMapProvider(providerOptions(this.props)); } componentWillUnmount() { if (this.timeoutId) { window.clearTimeout(this.timeoutId); } } handleTextInputChange(query: string) { if (this.timeoutId) { window.clearTimeout(this.timeoutId); } if (query.trim() !== '') { this.timeoutId = window.setTimeout( async () => { const searchResults = await this.searchProvider.search({ query }); if (this.state.query !== query) { return; } this.setState({ results: searchResults .map((result) => ({ id: result.raw.place_id, address: result.label, latLng: { lat: result.y, lng: result.x }, })), attributions: searchResults // eslint-disable-next-line max-len // @ts-expect-error discrepancy between leaflet-geosearch API (expects license) and the actual Nominatim data .map((result) => result.raw.licence) // filter out duplicate elements .filter((value, index, array) => array.indexOf(value) === index), }); }, this.props.searchDelay, ); this.setState({ query }); } else { this.setState({ query, results: [], attributions: [] }); } } handleClick(index: number) { this.props.addHandler(this.state.results[index]); this.setState({ query: '', results: [], attributions: [], }); // TODO focus text input } render() { const { attributions, query, results } = this.state; return ( <> {results.length > 0 && (
    {results.map((result, index) => (
  • {result.address}
  • ))}
  • {attributions.map((attribution) => (
  • {attribution}
  • ))}
)} ); } } export default SearchBox; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/Vehicle.test.tsx ================================================ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { VehicleCapacity } from 'store/route/types'; import { shallow, toJson } from 'ui/shallow-test-util'; import Vehicle, { VehicleProps } from './Vehicle'; describe('Vehicle Component', () => { it('should render correctly', () => { const props: VehicleProps = { id: 10, description: 'x', capacity: 7, removeHandler: jest.fn(), capacityChangeHandler: jest.fn(), }; const vehicle = shallow(); expect(toJson(vehicle)).toMatchSnapshot(); }); it('handlers', async () => { const props: VehicleProps = { id: 10, description: 'x', capacity: 7, removeHandler: jest.fn(), capacityChangeHandler: jest.fn(), }; render(); const user = userEvent.setup(); await user.click(screen.getByTestId(`remove-${props.id}`)); expect(props.removeHandler).toHaveBeenCalledTimes(1); await user.click(screen.getByTestId(`capacity-increase-${props.id}`)); const increasedCapacity: VehicleCapacity = { vehicleId: props.id, capacity: props.capacity + 1, }; expect(props.capacityChangeHandler).toHaveBeenCalledWith(increasedCapacity); await user.click(screen.getByTestId(`capacity-decrease-${props.id}`)); const decreasedCapacity: VehicleCapacity = { vehicleId: props.id, capacity: props.capacity - 1, }; expect(props.capacityChangeHandler).toHaveBeenCalledWith(decreasedCapacity); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/Vehicle.tsx ================================================ import { Button, ButtonVariant, DataListCell, DataListItem, DataListItemRow, InputGroup, InputGroupText, } from '@patternfly/react-core'; import { MinusIcon, PlusIcon, TimesIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { VehicleCapacity } from 'store/route/types'; export interface VehicleProps { id: number; description: string; capacity: number; removeHandler: (id: number) => void; capacityChangeHandler: (vehicleCapacity: VehicleCapacity) => void; } const Vehicle: React.FC = ({ id, description, capacity, removeHandler, capacityChangeHandler, }) => { const [clicked, setClicked] = React.useState(false); return ( {description} {capacity} ); }; export default Vehicle; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/Alerts.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Alerts should call readMessage() when alert is closed 1`] = ` Object { "asFragment": [Function], "baseElement":
  • Danger alert: Error

    msg 1
  • Danger alert: Error

    msg 2
, "container":
, "debug": [Function], "findAllByAltText": [Function], "findAllByDisplayValue": [Function], "findAllByLabelText": [Function], "findAllByPlaceholderText": [Function], "findAllByRole": [Function], "findAllByTestId": [Function], "findAllByText": [Function], "findAllByTitle": [Function], "findByAltText": [Function], "findByDisplayValue": [Function], "findByLabelText": [Function], "findByPlaceholderText": [Function], "findByRole": [Function], "findByTestId": [Function], "findByText": [Function], "findByTitle": [Function], "getAllByAltText": [Function], "getAllByDisplayValue": [Function], "getAllByLabelText": [Function], "getAllByPlaceholderText": [Function], "getAllByRole": [Function], "getAllByTestId": [Function], "getAllByText": [Function], "getAllByTitle": [Function], "getByAltText": [Function], "getByDisplayValue": [Function], "getByLabelText": [Function], "getByPlaceholderText": [Function], "getByRole": [Function], "getByTestId": [Function], "getByText": [Function], "getByTitle": [Function], "queryAllByAltText": [Function], "queryAllByDisplayValue": [Function], "queryAllByLabelText": [Function], "queryAllByPlaceholderText": [Function], "queryAllByRole": [Function], "queryAllByTestId": [Function], "queryAllByText": [Function], "queryAllByTitle": [Function], "queryByAltText": [Function], "queryByDisplayValue": [Function], "queryByLabelText": [Function], "queryByPlaceholderText": [Function], "queryByRole": [Function], "queryByTestId": [Function], "queryByText": [Function], "queryByTitle": [Function], "rerender": [Function], "unmount": [Function], } `; exports[`Alerts should not render if there are no messages 1`] = `null`; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/DemoDropdown.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Demo dropdown button should be disabled with empty demos 1`] = ` Load demo } /> `; exports[`Demo dropdown button should render correctly with a couple of demos 1`] = ` demo 1 , demo 2 , ] } isOpen={false} onSelect={[Function]} position="right" style={ Object { "marginBottom": 16, "marginLeft": 16, } } toggle={ Load demo } /> `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/Location.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Location Component should render correctly 1`] = ` x `; exports[`Location Component should render correctly when description is missing 1`] = ` Location 11 `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/LocationList.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Location List Component should render correctly with a few routes 1`] = `
`; exports[`Location List Component should render correctly with no routes 1`] = ` No locations `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/LocationMarker.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Location Marker render depot 1`] = ` Location 1 [Lat=1.345678, Lng=1.345678] `; exports[`Location Marker render visit 1`] = ` Location 1 [Lat=1.345678, Lng=1.345678] `; exports[`Location Marker selected visit should show a tooltip 1`] = ` Location 1 [Lat=1.345678, Lng=1.345678] `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/RouteMap.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Route Map should ignore bounds if viewport is dirty 1`] = ` `; exports[`Route Map should pan and zoom to show bounding box if viewport is not dirty 1`] = ` `; exports[`Route Map should show the whole world when bounding box is null 1`] = ` `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/SearchBox.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Search box should render text input initially 1`] = ` `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/components/__snapshots__/Vehicle.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Vehicle Component should render correctly 1`] = ` x 7 `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/connection/ConnectionError.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import ConnectionError, { ConnectionErrorProps } from './ConnectionError'; describe('Connection Error Component', () => { it('should render correctly', () => { const props: ConnectionErrorProps = { isOpen: true, }; const connectionError = shallow(); expect(toJson(connectionError)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/connection/ConnectionError.tsx ================================================ import { ExpandableSection, List, ListItem, Modal, Text, TextContent, TextVariants, } from '@patternfly/react-core'; import { backendUrl } from 'common'; import * as React from 'react'; export interface ConnectionErrorProps { isOpen: boolean; } const title = 'Connection error'; const ConnectionError: React.FC = ({ isOpen }) => ( The server is unreachable. Trying to reconnect. The server is expected to be running at {' '}
{backendUrl} {' '} but the connection failed. Please check the following possible reasons and try to resolve them: You are offline. Check your network connection. The server is running on a different URL. Check if the URL is incorrect. The server is down. Restart the server. The application will reconnect as soon as the server is available again. ); export default ConnectionError; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/connection/ConnectionManager.test.tsx ================================================ import { render } from '@testing-library/react'; import { WebSocketConnectionStatus } from 'store/websocket/types'; import { shallow, toJson } from 'ui/shallow-test-util'; import { ConnectionManager, Props } from './ConnectionManager'; describe('Connection Manager', () => { it('should connect the WebSocket client when mounted', () => { const props: Props = { connectClient: jest.fn(), connectionStatus: WebSocketConnectionStatus.CLOSED, }; render(); // Connect WebSocket client when the component is mounted expect(props.connectClient).toHaveBeenCalled(); }); it('should not display error when connection is closed', () => { const props: Props = { connectClient: jest.fn(), connectionStatus: WebSocketConnectionStatus.CLOSED, }; const connectionManager = shallow(); expect(toJson(connectionManager)).toMatchSnapshot(); }); it('should not display error when connection is open', () => { const props: Props = { connectClient: jest.fn(), connectionStatus: WebSocketConnectionStatus.OPEN, }; const connectionManager = shallow(); expect(toJson(connectionManager)).toMatchSnapshot(); }); it('should display error when connection fails', () => { const props: Props = { connectClient: jest.fn(), connectionStatus: WebSocketConnectionStatus.ERROR, }; const connectionManager = shallow(); expect(toJson(connectionManager)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/connection/ConnectionManager.tsx ================================================ import * as React from 'react'; import { connect } from 'react-redux'; import { AppState } from 'store/types'; import { websocketOperations } from 'store/websocket'; import { WebSocketConnectionStatus } from 'store/websocket/types'; import ConnectionError from 'ui/connection/ConnectionError'; interface StateProps { connectionStatus: WebSocketConnectionStatus; } interface DispatchProps { connectClient: typeof websocketOperations.connectClient; } export type Props = StateProps & DispatchProps; const mapStateToProps = ({ connectionStatus }: AppState): StateProps => ({ connectionStatus, }); const mapDispatchToProps: DispatchProps = { connectClient: websocketOperations.connectClient, }; export class ConnectionManager extends React.Component { componentDidMount() { this.props.connectClient(); } render() { return ( ); } } export default connect( mapStateToProps, mapDispatchToProps, )(ConnectionManager); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/connection/__snapshots__/ConnectionError.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Connection Error Component should render correctly 1`] = ` The server is unreachable. Trying to reconnect. The server is expected to be running at but the connection failed. Please check the following possible reasons and try to resolve them: You are offline. Check your network connection. The server is running on a different URL. Check if the URL is incorrect. The server is down. Restart the server. The application will reconnect as soon as the server is available again. `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/connection/__snapshots__/ConnectionManager.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Connection Manager should display error when connection fails 1`] = ` `; exports[`Connection Manager should not display error when connection is closed 1`] = ` `; exports[`Connection Manager should not display error when connection is open 1`] = ` `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/connection/index.ts ================================================ import ConnectedConnectionManager from './ConnectionManager'; export { ConnectedConnectionManager as ConnectionManager }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/header/Header.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import Header from './Header'; describe('Header component', () => { it('should match snapshot', () => { const header = shallow(
); expect(toJson(header)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/header/Header.tsx ================================================ import { Brand, PageHeader } from '@patternfly/react-core'; import * as React from 'react'; import NavigationWithRouter from './Navigation'; const Header: React.FC = () => ( } topNav={} /> ); export default Header; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/header/Navigation.test.tsx ================================================ import { Location } from 'history'; import { RouteComponentProps } from 'react-router'; import { shallow, toJson } from 'ui/shallow-test-util'; import { Navigation } from './Navigation'; describe('Navigation', () => { it('should activate a navigation link matching the current path', () => { const props: RouteComponentProps = { location: { pathname: '/visits', } as Location, } as RouteComponentProps; // const visitsId = '/visits'; const navigation = shallow(); expect(toJson(navigation)).toMatchSnapshot(); // FIXME // NavItem matching the path should be active // const navItems = navigation.find(NavItem).filterWhere((navItem) => navItem.props().itemId === visitsId); // expect(navItems).toHaveLength(1); // expect(navItems.at(0).props().isActive).toEqual(true); // Other NavItems should be inactive // navigation.find(NavItem).filterWhere((navItem) => navItem.props().itemId !== visitsId).forEach( // (navItem) => expect(navItem.props().isActive).toEqual(false), // ); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/header/Navigation.tsx ================================================ import { Nav, NavItem, NavList } from '@patternfly/react-core'; import * as React from 'react'; import { RouteComponentProps } from 'react-router'; import { Link, withRouter } from 'react-router-dom'; import { pagesByPath } from 'ui/App'; export const Navigation = ({ location }: RouteComponentProps) => ( ); export default withRouter(Navigation); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/header/__snapshots__/Header.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header component should match snapshot 1`] = ` } topNav={} /> `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/header/__snapshots__/Navigation.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Navigation should activate a navigation link matching the current path 1`] = ` `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Demo.test.tsx ================================================ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserViewport } from 'store/client/types'; import { shallow, toJson } from 'ui/shallow-test-util'; import { Demo, DemoProps } from './Demo'; describe('Demo page', () => { it('should render correctly with no routes', () => { const demo = shallow(); expect(toJson(demo)).toMatchSnapshot(); }); it('should render correctly with a few routes', () => { const demo = shallow(); expect(toJson(demo)).toMatchSnapshot(); }); it('clear and export buttons should be disabled when demo is loading', () => { const props: DemoProps = { ...threeLocationsProps, isDemoLoading: true, }; const demo = shallow(); expect(toJson(demo)).toMatchSnapshot(); }); // FIXME xit('clear and export buttons should be disabled when demo is loading', async () => { const props: DemoProps = { ...threeLocationsProps, isDemoLoading: true, }; const user = userEvent.setup(); render(); const clearButton = screen.getByRole('button', { name: 'Clear' }); expect(clearButton).toBeDisabled(); await user.click(clearButton); // Doesn't work, probably due to https://github.com/airbnb/enzyme/issues/386 expect(props.clearHandler).not.toHaveBeenCalled(); const exportButton = screen.getByRole('button', { name: 'Export' }); expect(exportButton).toBeDisabled(); }); it('clear button should replace demo dropdown as soon as there is a depot', () => { const props: DemoProps = { ...emptyRouteProps, depot: { id: 1, lat: 1, lng: 1, description: '', }, }; render(); const clearButton = screen.getByRole('button', { name: 'Clear' }); expect(clearButton).toBeEnabled(); const exportButton = screen.getByRole('button', { name: 'Export' }); expect(exportButton).toBeEnabled(); expect(screen.queryByRole('button', { name: 'Load demo' })).not.toBeInTheDocument(); }); }); const userViewport: UserViewport = { isDirty: false, zoom: 1, center: [0, 0], }; const emptyRouteProps: DemoProps = { loadHandler: jest.fn(), clearHandler: jest.fn(), addVehicleHandler: jest.fn, removeVehicleHandler: jest.fn, addLocationHandler: jest.fn(), removeLocationHandler: jest.fn(), updateViewport: jest.fn(), distance: '0', vehicleCount: 0, totalCapacity: 0, totalDemand: 0, demoNames: ['demo'], isDemoLoading: false, boundingBox: null, userViewport, countryCodeSearchFilter: [], depot: null, routes: [], visits: [], }; const threeLocationsProps: DemoProps = { loadHandler: jest.fn(), clearHandler: jest.fn(), addVehicleHandler: jest.fn, removeVehicleHandler: jest.fn, addLocationHandler: jest.fn(), removeLocationHandler: jest.fn(), updateViewport: jest.fn(), distance: '10', vehicleCount: 8, totalCapacity: 5, totalDemand: 2, demoNames: ['demo'], isDemoLoading: false, boundingBox: null, userViewport, countryCodeSearchFilter: ['XY'], depot: { id: 1, lat: 1.345678, lng: 1.345678, }, visits: [{ id: 2, lat: 2.345678, lng: 2.345678, }, { id: 3, lat: 3.676111, lng: 3.568333, }], routes: [{ vehicle: { id: 1, name: 'v1', capacity: 5 }, visits: [{ id: 1, lat: 1.345678, lng: 1.345678, }, { id: 2, lat: 2.345678, lng: 2.345678, }, { id: 3, lat: 3.676111, lng: 3.568333, }], track: [], }], }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Demo.tsx ================================================ import { Button, ButtonVariant, Flex, FlexItem, InputGroup, InputGroupText, Split, SplitItem, Title, } from '@patternfly/react-core'; import { MinusIcon, PlusIcon } from '@patternfly/react-icons'; import { backendUrl } from 'common'; import { LeafletMouseEvent } from 'leaflet'; import * as React from 'react'; import { connect } from 'react-redux'; import { clientOperations } from 'store/client'; import { UserViewport } from 'store/client/types'; import { demoOperations } from 'store/demo'; import { routeOperations, routeSelectors } from 'store/route'; import { Location, RouteWithTrack } from 'store/route/types'; import { BoundingBox } from 'store/server/types'; import { AppState } from 'store/types'; import { DemoDropdown } from 'ui/components/DemoDropdown'; import LocationList from 'ui/components/LocationList'; import RouteMap from 'ui/components/RouteMap'; import SearchBox, { Result } from 'ui/components/SearchBox'; import { sideBarStyle } from 'ui/pages/common'; import { CapacityInfo, DistanceInfo, VehiclesInfo, VisitsInfo } from 'ui/pages/InfoBlock'; export interface StateProps { distance: string; depot: Location | null; vehicleCount: number; totalCapacity: number; totalDemand: number; visits: Location[]; routes: RouteWithTrack[]; isDemoLoading: boolean; boundingBox: BoundingBox | null; userViewport: UserViewport; countryCodeSearchFilter: string[]; demoNames: string[]; } export interface DispatchProps { loadHandler: typeof demoOperations.requestDemo; clearHandler: typeof routeOperations.clearRoute; addLocationHandler: typeof routeOperations.addLocation; removeLocationHandler: typeof routeOperations.deleteLocation; addVehicleHandler: typeof routeOperations.addVehicle; removeVehicleHandler: typeof routeOperations.deleteAnyVehicle; updateViewport: typeof clientOperations.updateViewport; } const mapStateToProps = ({ plan, demo, serverInfo, userViewport }: AppState): StateProps => ({ distance: plan.distance, vehicleCount: plan.vehicles.length, totalCapacity: routeSelectors.totalCapacity(plan), totalDemand: routeSelectors.totalDemand(plan), depot: plan.depot, visits: plan.visits, routes: plan.routes, isDemoLoading: demo.isLoading, boundingBox: serverInfo.boundingBox, countryCodeSearchFilter: serverInfo.countryCodes, // TODO use selector // TODO sort demos alphabetically? demoNames: (serverInfo.demos && serverInfo.demos.map((value) => value.name)) || [], userViewport, }); const mapDispatchToProps: DispatchProps = { loadHandler: demoOperations.requestDemo, clearHandler: routeOperations.clearRoute, addLocationHandler: routeOperations.addLocation, removeLocationHandler: routeOperations.deleteLocation, addVehicleHandler: routeOperations.addVehicle, removeVehicleHandler: routeOperations.deleteAnyVehicle, updateViewport: clientOperations.updateViewport, }; export type DemoProps = DispatchProps & StateProps; export interface DemoState { selectedId: number; } export class Demo extends React.Component { constructor(props: DemoProps) { super(props); this.state = { selectedId: NaN, }; this.handleDemoLoadClick = this.handleDemoLoadClick.bind(this); this.handleMapClick = this.handleMapClick.bind(this); this.handleSearchResultClick = this.handleSearchResultClick.bind(this); this.onSelectLocation = this.onSelectLocation.bind(this); } handleMapClick(e: LeafletMouseEvent) { this.props.addLocationHandler({ ...e.latlng, description: '' }); // TODO use reverse geocoding to find address } handleSearchResultClick(result: Result) { this.props.addLocationHandler({ ...result.latLng, description: result.address }); } handleDemoLoadClick(demoName: string) { this.props.loadHandler(demoName); } onSelectLocation(id: number) { this.setState({ selectedId: id }); } render() { const { selectedId } = this.state; const { distance, depot, vehicleCount, totalCapacity, totalDemand, visits, routes, demoNames, isDemoLoading, boundingBox, userViewport, countryCodeSearchFilter, addVehicleHandler, removeVehicleHandler, removeLocationHandler, clearHandler, updateViewport, } = this.props; const exportDataSet = () => { window.open(`${backendUrl}/api/dataset/export`); }; return ( // FIXME find a way to avoid these style customizations Demo {vehicleCount} {(depot === null && ( )) || ( )} ); } } export default connect( mapStateToProps, mapDispatchToProps, )(Demo); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/InfoBlock.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import { PlusIcon } from '@patternfly/react-icons'; import { CapacityInfo, DistanceInfo, InfoBlock, VehiclesInfo, VisitsInfo } from 'ui/pages/InfoBlock'; describe('Info block snapshots:', () => { it('generic', () => { const infoBlock = shallow( , ); expect(toJson(infoBlock)).toMatchSnapshot(); }); it('capacity', () => { const capacityInfoOK = shallow(); expect(toJson(capacityInfoOK)).toMatchSnapshot(); const capacityInfoError = shallow(); expect(toJson(capacityInfoError)).toMatchSnapshot(); }); it('distance', () => { const distanceInfo = shallow(); expect(toJson(distanceInfo)).toMatchSnapshot(); }); it('vehicles', () => { const vehiclesInfo = shallow(); expect(toJson(vehiclesInfo)).toMatchSnapshot(); }); it('visits', () => { const visitsInfo = shallow(); expect(toJson(visitsInfo)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/InfoBlock.tsx ================================================ import { Flex, FlexItem, Tooltip } from '@patternfly/react-core'; import { ClockIcon as DistanceIcon, IconSize, MapMarkerIcon as VisitIcon, TruckIcon, WeightHangingIcon as CapacityIcon, } from '@patternfly/react-icons'; import { SVGIconProps } from '@patternfly/react-icons/dist/js/createIcon'; import * as React from 'react'; interface InfoBlockProps { icon: React.ComponentClass; content?: { data: string | number; minWidth: string; }; color?: string; tooltip: string; } export const InfoBlock = ({ icon, content, tooltip, color }: InfoBlockProps) => { const Icon = icon; return ( {content && ( {content.data} )} ); }; interface CapacityInfoProps { totalDemand: number; totalCapacity: number; } export const CapacityInfo = ({ totalDemand, totalCapacity, }: CapacityInfoProps) => ( totalCapacity ? 'var(--pf-global--danger-color--200)' : ''} tooltip="Capacity usage: total demand / total capacity" /> ); interface DistanceInfoProps { distance: string; } export const DistanceInfo = ({ distance }: DistanceInfoProps) => ( ); export const VehiclesInfo = () => ( ); interface VisitInfoProps { visitCount: number; } export const VisitsInfo = ({ visitCount }: VisitInfoProps) => ( ); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Route.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import { UserViewport } from 'store/client/types'; import { Route, RouteProps } from './Route'; describe('Route page', () => { it('should render correctly with no routes', () => { const routes = shallow(); expect(toJson(routes)).toMatchSnapshot(); }); it('should render correctly with a few routes', () => { const routes = shallow(); expect(toJson(routes)).toMatchSnapshot(); }); }); const userViewport: UserViewport = { isDirty: false, zoom: 1, center: [0, 0], }; const noRoutes: RouteProps = { addHandler: jest.fn(), removeHandler: jest.fn(), updateViewport: jest.fn(), boundingBox: null, userViewport, depot: null, visits: [], routes: [], }; const depot = { id: 1, lat: 1.345678, lng: 1.345678, }; const visit2 = { id: 2, lat: 2.345678, lng: 2.345678, }; const visit3 = { id: 3, lat: 3.676111, lng: 3.568333, }; const visit4 = { id: 4, lat: 4.345678, lng: 4.345678, }; const visit5 = { id: 5, lat: 5.345678, lng: 5.345678, }; const twoRoutes: RouteProps = { addHandler: jest.fn(), removeHandler: jest.fn(), updateViewport: jest.fn(), boundingBox: null, userViewport, depot, visits: [visit2, visit3, visit4, visit5], routes: [{ vehicle: { id: 1, name: 'v1', capacity: 5 }, visits: [depot, visit2, visit3], track: [], }, { vehicle: { id: 2, name: 'v2', capacity: 5 }, visits: [depot, visit4, visit5], track: [], }], }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Route.tsx ================================================ import { Form, FormSelect, FormSelectOption, Split, SplitItem, Title, } from '@patternfly/react-core'; import { LeafletMouseEvent } from 'leaflet'; import * as React from 'react'; import { connect } from 'react-redux'; import { clientOperations } from 'store/client'; import { UserViewport } from 'store/client/types'; import { routeOperations } from 'store/route'; import { Location, RouteWithTrack } from 'store/route/types'; import { BoundingBox } from 'store/server/types'; import { AppState } from 'store/types'; import LocationList from 'ui/components/LocationList'; import RouteMap from 'ui/components/RouteMap'; import { sideBarStyle } from 'ui/pages/common'; export interface StateProps { depot: Location | null; visits: Location[]; routes: RouteWithTrack[]; boundingBox: BoundingBox | null; userViewport: UserViewport; } export interface DispatchProps { addHandler: typeof routeOperations.addLocation; removeHandler: typeof routeOperations.deleteLocation; updateViewport: typeof clientOperations.updateViewport; } const mapStateToProps = ({ plan, serverInfo, userViewport }: AppState): StateProps => ({ depot: plan.depot, visits: plan.visits, routes: plan.routes, boundingBox: serverInfo.boundingBox, userViewport, }); const mapDispatchToProps: DispatchProps = { addHandler: routeOperations.addLocation, removeHandler: routeOperations.deleteLocation, updateViewport: clientOperations.updateViewport, }; export type RouteProps = DispatchProps & StateProps; export interface RouteState { selectedId: number; selectedRouteId: number; } export class Route extends React.Component { constructor(props: RouteProps) { super(props); this.state = { selectedId: NaN, selectedRouteId: 0, }; this.onSelectLocation = this.onSelectLocation.bind(this); this.handleMapClick = this.handleMapClick.bind(this); } handleMapClick(e: LeafletMouseEvent) { this.props.addHandler({ ...e.latlng, description: '' }); } onSelectLocation(id: number) { this.setState({ selectedId: id }); } render() { const { selectedId, selectedRouteId } = this.state; const { boundingBox, userViewport, depot, visits, routes, removeHandler, updateViewport, } = this.props; // FIXME quick hack to preserve route color by keeping its index const filteredRoutes = ( routes.map((value, index) => (index === selectedRouteId ? value : { visits: [], track: [] })) ); const filteredVisits: Location[] = routes.length > 0 ? routes[selectedRouteId].visits : []; return ( <> Route
{ this.setState({ selectedRouteId: parseInt(e as unknown as string, 10) }); }} aria-label="FormSelect Input" > {routes.map( (route, index) => ( ), )}
); } } export default connect( mapStateToProps, mapDispatchToProps, )(Route); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Vehicles.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import { Props, Vehicles } from './Vehicles'; describe('Vehicles page', () => { it('should render correctly', () => { const props: Props = { addVehicleHandler: jest.fn(), removeVehicleHandler: jest.fn(), changeVehicleCapacityHandler: jest.fn, vehicles: [ { id: 1, name: 'Vehicle 1', capacity: 5 }, { id: 2, name: 'Vehicle 2', capacity: 5 }, ], }; const vehicles = shallow(); expect(toJson(vehicles)).toMatchSnapshot(); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Vehicles.tsx ================================================ import { Button, DataList, Split, SplitItem, Title, } from '@patternfly/react-core'; import * as React from 'react'; import { connect } from 'react-redux'; import { routeOperations } from 'store/route'; import { Vehicle } from 'store/route/types'; import { AppState } from 'store/types'; import VehicleItem from 'ui/components/Vehicle'; interface StateProps { vehicles: Vehicle[]; } interface DispatchProps { addVehicleHandler: typeof routeOperations.addVehicle; removeVehicleHandler: typeof routeOperations.deleteVehicle; changeVehicleCapacityHandler: typeof routeOperations.changeVehicleCapacity; } export type Props = StateProps & DispatchProps; const mapStateToProps = ({ plan }: AppState): StateProps => ({ vehicles: plan.vehicles, }); const mapDispatchToProps: DispatchProps = { addVehicleHandler: routeOperations.addVehicle, removeVehicleHandler: routeOperations.deleteVehicle, changeVehicleCapacityHandler: routeOperations.changeVehicleCapacity, }; export const Vehicles: React.FC = ({ vehicles, addVehicleHandler, removeVehicleHandler, changeVehicleCapacityHandler, }) => ( <> {`Vehicles (${vehicles.length})`}
{vehicles .slice(0) // clone the array because // sort is done in place (that would affect the route) .sort((a, b) => a.id - b.id) .map((vehicle) => ( ))}
); export default connect(mapStateToProps, mapDispatchToProps)(Vehicles); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Visits.test.tsx ================================================ import { shallow, toJson } from 'ui/shallow-test-util'; import { Props, Visits } from './Visits'; describe('Visits page', () => { it('should render correctly with no visits', () => { const visits = shallow(); expect(toJson(visits)).toMatchSnapshot(); }); it('should render correctly with a few visits', () => { const visits = shallow(); expect(toJson(visits)).toMatchSnapshot(); }); }); const noVisits: Props = { removeHandler: jest.fn(), depot: null, visits: [], }; const twoVisits: Props = { removeHandler: jest.fn(), depot: { id: 1, lat: 1.345678, lng: 1.345678, }, visits: [{ id: 2, lat: 2.345678, lng: 2.345678, }, { id: 3, lat: 3.676111, lng: 3.568333, }], }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/Visits.tsx ================================================ import { Title } from '@patternfly/react-core'; import * as React from 'react'; import { connect } from 'react-redux'; import { routeOperations } from 'store/route'; import { Location } from 'store/route/types'; import { AppState } from 'store/types'; import LocationList from 'ui/components/LocationList'; interface StateProps { depot: Location | null; visits: Location[]; } const mapStateToProps = ({ plan }: AppState): StateProps => ({ depot: plan.depot, visits: plan.visits, }); export interface DispatchProps { removeHandler: typeof routeOperations.deleteLocation; } const mapDispatchToProps: DispatchProps = { removeHandler: routeOperations.deleteLocation, }; export type Props = StateProps & DispatchProps; export const Visits: React.FC = ({ depot, visits, removeHandler, }: Props) => ( <> {`Visits (${visits.length})`} {/* TODO do not show depots */} undefined} depot={depot} visits={visits} /> ); export default connect(mapStateToProps, mapDispatchToProps)(Visits); ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/__snapshots__/Demo.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Demo page clear and export buttons should be disabled when demo is loading 1`] = ` Demo 8 `; exports[`Demo page should render correctly with a few routes 1`] = ` Demo 8 `; exports[`Demo page should render correctly with no routes 1`] = ` Demo 0 `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/__snapshots__/InfoBlock.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Info block snapshots: capacity 1`] = ` `; exports[`Info block snapshots: capacity 2`] = ` `; exports[`Info block snapshots: distance 1`] = ` `; exports[`Info block snapshots: generic 1`] = ` test content `; exports[`Info block snapshots: vehicles 1`] = ` `; exports[`Info block snapshots: visits 1`] = ` `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/__snapshots__/Route.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Route page should render correctly with a few routes 1`] = ` Route
`; exports[`Route page should render correctly with no routes 1`] = ` Route
`; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/__snapshots__/Vehicles.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Vehicles page should render correctly 1`] = ` Vehicles (2)
`; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/__snapshots__/Visits.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Visits page should render correctly with a few visits 1`] = ` Visits (2) `; exports[`Visits page should render correctly with no visits 1`] = ` Visits (0) `; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/common.ts ================================================ import * as React from 'react'; export const sideBarStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', width: '320px', }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/pages/index.ts ================================================ import ConnectedDemo from './Demo'; import ConnectedRoute from './Route'; import ConnectedVehicles from './Vehicles'; import ConnectedVisits from './Visits'; export { ConnectedDemo as Demo, ConnectedVehicles as Vehicles, ConnectedVisits as Visits, ConnectedRoute as Route, }; ================================================ FILE: optaweb-vehicle-routing-frontend/src/ui/shallow-test-util.ts ================================================ import { ReactElement } from 'react'; // eslint-disable-next-line import/no-extraneous-dependencies import { createRenderer } from 'react-test-renderer/shallow'; export const shallow = (e: ReactElement): ReactElement => { const shallowRenderer = createRenderer(); shallowRenderer.render(e); return shallowRenderer.getRenderOutput(); }; export const toJson = (e: ReactElement): ReactElement => e; ================================================ FILE: optaweb-vehicle-routing-frontend/src/websocket/WebSocketClient.test.ts ================================================ import { sources } from 'eventsourcemock'; import fetchMock from 'fetch-mock-jest'; import { LatLngWithDescription } from 'store/route/types'; import WebSocketClient from './WebSocketClient'; beforeEach(() => { // Learn about fetch-mock: http://www.wheresrhys.co.uk/fetch-mock/. fetchMock.reset(); }); describe('WebSocketClient', () => { const url = 'http://test.url:123/my-endpoint'; const onSuccess = jest.fn(); const onError = jest.fn(); const connectClient = () => { const client = new WebSocketClient(url); client.connect(onSuccess, onError); const source = sources[`${url}/events`]; source.emitOpen(); return { client, source }; }; it('Error callback should be called on EventSource error event', () => { const { source } = connectClient(); source.onerror(); expect(onError).toBeCalled(); expect(source); }); it('Success callback should be called on EventSource open event', () => { const { source } = connectClient(); source.onopen(); expect(onSuccess).toBeCalled(); }); it('addLocation() should send location', () => { const location: LatLngWithDescription = { lat: 1, lng: 2, description: 'test', }; fetchMock.postOnce('*', 200); const { client } = connectClient(); client.addLocation(location); expect(fetchMock).toHaveLastFetched(`${url}/location`, { body: location }); }); it('deleteLocation() should send location ID', () => { const locationId = 21; fetchMock.deleteOnce('*', 200); const { client } = connectClient(); client.deleteLocation(locationId); expect(fetchMock).toHaveLastFetched(`${url}/location/${locationId}`); }); it('addVehicle() should add vehicle', () => { fetchMock.postOnce('*', 200); const { client } = connectClient(); client.addVehicle(); expect(fetchMock).toHaveLastFetched(`${url}/vehicle`); }); it('deleteVehicle() should send vehicle ID', () => { const vehicleId = 34; fetchMock.deleteOnce('*', 200); const { client } = connectClient(); client.deleteVehicle(vehicleId); expect(fetchMock).toHaveLastFetched(`${url}/vehicle/${vehicleId}`); }); it('deleteAnyVehicle() should send message to the correct destination', () => { fetchMock.postOnce('*', 200); const { client } = connectClient(); client.deleteAnyVehicle(); expect(fetchMock).toHaveLastFetched(`${url}/vehicle/deleteAny`); }); it('changeVehicleCapacity() should change capacity', () => { const vehicleId = 7; const capacity = 54; fetchMock.postOnce('*', 200); const { client } = connectClient(); client.changeVehicleCapacity(vehicleId, capacity); expect(fetchMock).toHaveLastFetched(`${url}/vehicle/${vehicleId}/capacity`, { body: capacity as unknown as undefined, }); }); it('loadDemo() should send demo name', () => { const demo = 'Test demo'; fetchMock.postOnce('*', 200); const { client } = connectClient(); client.loadDemo(demo); expect(fetchMock).toHaveLastFetched(`${url}/demo/${demo}`); }); it('clear() should call clear endpoint', () => { fetchMock.postOnce('*', 200); const { client } = connectClient(); client.clear(); expect(fetchMock).toHaveLastFetched(`${url}/clear`); }); it('subscribeToServerInfo() should subscribe with callback', async () => { const callback = jest.fn(); const payload = { value: 'test' }; fetchMock.getOnce(`${url}/serverInfo`, { status: 200, body: JSON.stringify(payload), }); const { client } = connectClient(); await client.subscribeToServerInfo(callback); expect(fetchMock).toBeDone(); expect(callback).toHaveBeenCalledWith(payload); }); it('subscribeToRoute() should subscribe with callback', () => { const callback = jest.fn(); const payload = { msg: 'test' }; const messageEvent = new MessageEvent('route', { data: JSON.stringify(payload), }); const { client, source } = connectClient(); client.subscribeToRoute(callback); source.emit(messageEvent.type, messageEvent); expect(callback).toHaveBeenCalledWith(payload); }); it('subscribeToErrorTopic() should subscribe with callback', () => { const callback = jest.fn(); const payload = { msg: 'test' }; const messageEvent = new MessageEvent('errorMessage', { data: JSON.stringify(payload), }); const { client, source } = connectClient(); client.subscribeToErrorTopic(callback); source.emit(messageEvent.type, messageEvent); expect(callback).toHaveBeenCalledWith(payload); }); }); ================================================ FILE: optaweb-vehicle-routing-frontend/src/websocket/WebSocketClient.ts ================================================ import { MessagePayload } from 'store/message/types'; import { LatLngWithDescription, RoutingPlan } from 'store/route/types'; import { ServerInfo } from 'store/server/types'; export default class WebSocketClient { readonly backendUrl: string; eventSource: EventSource | null; constructor(backendUrl: string) { this.backendUrl = backendUrl; this.eventSource = null; } connect(successCallback: () => void, errorCallback: (err: Event) => void): void { if (this.eventSource === null) { this.eventSource = new EventSource(`${this.backendUrl}/events`); this.eventSource.onopen = successCallback; this.eventSource.onerror = (event) => { // Each time a connection error happens... if (this.eventSource) { // ...close the eventSource... this.eventSource.close(); this.eventSource = null; } // ...and invoke the errorCallback, which dispatches a single connectClient thunk action. // That forms an infinite loop, so the connection will be re-attempted until it succeeds. errorCallback((event)); }; } } addLocation(latLng: LatLngWithDescription): Promise { return fetch(`${this.backendUrl}/location`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(latLng), }); } addVehicle(): Promise { return fetch(`${this.backendUrl}/vehicle`, { method: 'POST' }); } loadDemo(name: string): Promise { return fetch(`${this.backendUrl}/demo/${name}`, { method: 'POST' }); } deleteLocation(locationId: number): Promise { // TODO error callback return fetch(`${this.backendUrl}/location/${locationId}`, { method: 'DELETE' }); } deleteAnyVehicle(): Promise { return fetch(`${this.backendUrl}/vehicle/deleteAny`, { method: 'POST' }); } deleteVehicle(vehicleId: number): Promise { return fetch(`${this.backendUrl}/vehicle/${vehicleId}`, { method: 'DELETE' }); } changeVehicleCapacity(vehicleId: number, capacity: number): Promise { return fetch(`${this.backendUrl}/vehicle/${vehicleId}/capacity`, { method: 'POST', body: JSON.stringify(capacity), }); } clear(): Promise { return fetch(`${this.backendUrl}/clear`, { method: 'POST' }); } subscribeToServerInfo(subscriptionCallback: (serverInfo: ServerInfo) => void): Promise { return fetch(`${this.backendUrl}/serverInfo`) .then((response) => response.json()) .then((data) => subscriptionCallback(data)); } subscribeToRoute(subscriptionCallback: (plan: RoutingPlan) => void): void { if (this.eventSource !== null) { this.eventSource.addEventListener('route', (event: MessageEvent) => { subscriptionCallback(JSON.parse(event.data)); }); } } subscribeToErrorTopic(subscriptionCallback: (errorMessage: MessagePayload) => void): void { if (this.eventSource !== null) { this.eventSource.addEventListener('errorMessage', (event: MessageEvent) => { subscriptionCallback(JSON.parse(event.data)); }); } } } ================================================ FILE: optaweb-vehicle-routing-frontend/tsconfig.json ================================================ { "compilerOptions": { "baseUrl": "src", "outDir": "build/dist", "module": "esnext", "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "isolatedModules": true, "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "jsx": "react-jsx", "allowJs": true, "strict": true, "strictFunctionTypes": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true }, "include": [ "cypress", "src" ], "exclude": [ "build", "coverage", "local", "node_modules" ] } ================================================ FILE: optaweb-vehicle-routing-standalone/.gitignore ================================================ /target /local ================================================ FILE: optaweb-vehicle-routing-standalone/data/openstreetmap/CREDITS.adoc ================================================ All `.osm.pbf` files in this folder contain data link:https://www.openstreetmap.org/copyright[copyrighted by OpenStreetMap contributors] and licensed under link:https://opendatacommons.org/licenses/odbl/[Open Database License (ODbL)]. ================================================ FILE: optaweb-vehicle-routing-standalone/pom.xml ================================================ 4.0.0 org.optaweb.vehiclerouting optaweb-vehicle-routing 8.35.0.Final optaweb-vehicle-routing-standalone jar OptaWeb Vehicle Routing Standalone localhost 8180 http://${application.host}:${application.port} docker optaweb-vehicle-routing-frontend org.optaweb.vehiclerouting 7.0.1 org.optaweb.vehiclerouting optaweb-vehicle-routing-backend org.optaweb.vehiclerouting optaweb-vehicle-routing-frontend war io.quarkus quarkus-undertow org.apache.maven.plugins maven-dependency-plugin unpack-frontend prepare-package unpack org.optaweb.vehiclerouting optaweb-vehicle-routing-frontend war ${project.build.outputDirectory}/META-INF/resources META-INF/**,WEB-INF/** io.quarkus quarkus-maven-plugin ${version.io.quarkus} true build org.apache.maven.plugins maven-assembly-plugin package-quarkus-app package single src/main/assembly/assembly-quarkus-app.xml com.bazaarvoice.maven.plugins process-exec-maven-plugin 0.9 start-application pre-integration-test start Run application ${application.url} ${project.basedir} ${project.build.directory}/${project.artifactId}.log java -Dquarkus.profile=cypress -Dquarkus.http.host=${application.host} -Dquarkus.http.port=${application.port} -jar target/quarkus-app/quarkus-run.jar stop-running-processes post-integration-test stop-all exec-maven-plugin org.codehaus.mojo run-cypress-tests integration-test exec ${container.runtime} ${project.parent.basedir}/${frontend.project.name} run --network=host --volume ${project.parent.basedir}/${frontend.project.name}:/e2e:Z --workdir /e2e ${user.flag} ${user.name.group} --entrypoint cypress docker.io/cypress/included:${version.cypress.docker} run --project . --config baseUrl=${application.url} skip-integration-tests integration-tests !true com.bazaarvoice.maven.plugins process-exec-maven-plugin start-application none exec-maven-plugin org.codehaus.mojo run-cypress-tests none no-docker env.GITHUB_ACTIONS true Windows com.bazaarvoice.maven.plugins process-exec-maven-plugin start-application none exec-maven-plugin org.codehaus.mojo run-cypress-tests none docker container.runtime docker --user 1000:1000 ================================================ FILE: optaweb-vehicle-routing-standalone/src/main/assembly/assembly-quarkus-app.xml ================================================ quarkus-app zip true target/quarkus-app ================================================ FILE: optaweb-vehicle-routing-standalone/src/main/resources/META-INF/undertow-handlers.conf ================================================ path-prefix(/api/) -> done regex('/\\w+') -> rewrite(/) ================================================ FILE: optaweb-vehicle-routing-standalone/src/main/resources/application.properties ================================================ # App configuration app.demo.data-set-dir=local/dataset app.routing.gh-dir=local/graphhopper app.routing.osm-dir=local/openstreetmap app.routing.engine=GRAPHHOPPER app.routing.osm-file=belgium-latest.osm.pbf app.region.country-codes=BE # OptaPlanner quarkus.optaplanner.solver.daemon=true quarkus.optaplanner.solver.termination.spent-limit=30s # Datasource quarkus.datasource.db-kind=h2 quarkus.datasource.jdbc.url=jdbc:h2:file:${app.persistence.h2-dir:../local/db}/${app.persistence.h2-filename:optaweb_vrp_database};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false quarkus.datasource.username=sa quarkus.datasource.password= quarkus.hibernate-orm.database.generation=update %postgresql.quarkus.datasource.db-kind=postgresql %postgresql.quarkus.datasource.jdbc.url=jdbc:postgresql://${DATABASE_HOST:postgresql}:5432/${DATABASE_NAME:optaweb_vrp_database} %postgresql.quarkus.datasource.username=${DATABASE_USER} %postgresql.quarkus.datasource.password=${DATABASE_PASSWORD} %postgresql.quarkus.hibernate-orm.database.generation=update %cypress.app.region.country-codes=DE %cypress.app.routing.gh-dir=target/graphhopper %cypress.app.routing.osm-dir=data/openstreetmap %cypress.app.routing.osm-file=planet_12.032,53.0171_12.1024,53.0491.osm.pbf %cypress.quarkus.datasource.jdbc.url=jdbc:h2:mem:vehicle-routing-test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE # Quarkus configuration # Use fast-jar packaging (https://quarkus.io/guides/maven-tooling#using-fast-jar). quarkus.package.type=fast-jar # Enable CORS filter (https://quarkus.io/guides/http-reference#cors-filter). quarkus.http.cors=true ================================================ FILE: pom.xml ================================================ 4.0.0 org.optaplanner optaplanner-build-parent 8.35.0.Final org.optaweb.vehiclerouting optaweb-vehicle-routing pom OptaWeb Vehicle Routing optaweb-vehicle-routing-backend optaweb-vehicle-routing-frontend optaweb-vehicle-routing-standalone optaweb-vehicle-routing-docs optaweb-vehicle-routing-distribution 6.0 1.27 1.12.1 v16.2.0 7.15.1 org.optaweb.vehiclerouting optaweb-vehicle-routing-backend ${project.version} org.optaweb.vehiclerouting optaweb-vehicle-routing-docs ${project.version} zip org.optaweb.vehiclerouting optaweb-vehicle-routing-frontend ${project.version} war org.optaweb.vehiclerouting optaweb-vehicle-routing-standalone ${project.version} quarkus-app zip io.quarkus quarkus-bom ${version.io.quarkus} pom import com.graphhopper graphhopper-core ${version.com.graphhopper} com.neovisionaries nv-i18n ${version.com.neovisionaries} org.apache.maven.plugins maven-dependency-plugin analyze-only none jboss-public-repository-group JBoss Public Repository Group https://repository.jboss.org/nexus/content/groups/public/ default true never true daily ================================================ FILE: runLocally.sh ================================================ #!/usr/bin/env bash # Abort the script if any simple command outside an if, while, &&, ||, etc. exits with a non-zero status. set -e # If dir is empty, dir/* will expand to "" instead of "dir/*". This is useful when reading regions in interactive() and # either openstreetmap or graphhopper dir is empty. shopt -s nullglob function confirm() { declare answer read -r -p "$1 [y/N]: " answer [[ "$answer" == "y" ]] } function abort() { echo "Aborted." exit 0 } function standalone_jar_or_maven() { local -r standalone=optaweb-vehicle-routing-standalone # BEGIN: Distribution use case # # We're running a copy of the script in the project root that has been moved to distribution's bin directory during # distribution assembly. The only difference is that the standalone JAR is in the same directory as the script (bin) # and project.version is set using resource filtering during assembly. # shellcheck disable=SC2154 #(project.version variable is not declared) if [[ ! -f pom.xml && -f ${standalone}-${project.version}/quarkus-run.jar ]] then readonly jar=${standalone}-${project.version}/quarkus-run.jar return 0 fi # END: Distribution use case readonly jar=${standalone}/target/quarkus-app/quarkus-run.jar if [[ ! -f ${jar} ]] then confirm "Jarfile '$jar' does not exist. Run Maven build now?" || abort if ! ./mvnw clean install -DskipTests then echo >&2 "Maven build failed. Aborting the script." exit 1 fi fi } function validate() { local -r osm_file_path=${osm_dir}/${osm_file} local -r gh_graph_path=${gh_dir}/${osm_file%.osm.pbf} [[ -f "$osm_file_path" || -d "$gh_graph_path" ]] } function run_optaweb() { declare -a args args+=("-Dapp.demo.data-set-dir=$dataset_dir") args+=("-Dapp.persistence.h2-dir=$vrp_dir/db") args+=("-Dapp.persistence.h2-filename=${osm_file%.osm.pbf}") args+=("-Dapp.routing.engine=$routing_engine") if [[ ${routing_engine} == "GRAPHHOPPER" ]] then args+=("-Dapp.routing.osm-dir=$osm_dir") args+=("-Dapp.routing.gh-dir=$gh_dir") args+=("-Dapp.routing.osm-file=$osm_file") fi [[ ${cc_list} != "??" ]] && args+=("-Dapp.region.country-codes=$cc_list") java "${args[@]}" "$@" -jar "$jar" } function download() { echo "Downloading $1..." curl -L "$1" -o "$2" echo echo "Created $2." } function country_code() { local -r region=${1%.osm.pbf} local -r cc_file=${cc_dir}/${region} local -r cc_tag="nv-i18n-1.27" local -r cc_java="$cache_dir/CountryCode-$cc_tag.java" # If an error has occurred in the list_downloads loop, mark this region's code as "unknown". [[ $2 == "ERROR" ]] && echo "??" > "$cc_file" if [[ (! -f ${cc_java} || -f ${cc_java}.err) && $2 != "ERROR" ]] then if curl 2>>"$cc_java.err" > "$cc_java" --silent --show-error \ https://raw.githubusercontent.com/TakahikoKawasaki/nv-i18n/${cc_tag}/src/main/java/com/neovisionaries/i18n/CountryCode.java then rm "$cc_java.err" else # mark this region's code as "unknown" [[ ! -f ${cc_file} ]] && echo "??" > "$cc_file" # and report error return 1 fi fi # If this loop instance doesn't have an error and cc_file doesn't exist yet or its content is "unknown". if [[ $2 != ERROR && ( (! -f ${cc_file}) || $(cat "$cc_file") == "??" ) ]] then local region_search=${region%-latest} region_search=${region_search//-/ } cc=$(grep -i "$region_search.*OFFICIALLY_ASSIGNED" "$cc_java" | sed 's/ *\(..\).*/\1/') echo "$cc" > "$cc_file" fi } function download_menu() { local -r url=$1 local -r url_parent=${url%/*} # remove shortest suffix matching "/*" => https://download.geofabrik.de/north-america/us local -r region_filename=${url##*/} # index.html, europe.html, etc. local -r region_file_html="$cache_geofabrik/$region_filename" local -r region_file_csv=${region_file_html/.html/.csv} local -r region_osm_url=$2 # TODO refresh daily if [[ ! -f ${region_file_html} || ! -s ${region_file_html} ]] then curl --silent --show-error 2>>"$cache_geofabrik/error.log" "$url" > "$region_file_html" || { echo "ERROR: Cannot download from Geofabrik. Are you offline?" exit 1 } fi # The following AWK program subregion information from a Geofabrik region HTML page. # # If Geofabrik offers subregions for the current region, the region HTML page contains a subregion table. The program # goes over all subregion rows and extracts the following data: # 1. subregion name (example: Europe), # 2. subregion page link (example: europe.html), # 3. subregion OSM link (example: europe-latest.osm.pbf), # 4. subregion OSM size (example: 23.1 GB). # Finally, the program prints the data in a format that makes it possible to read the data into a Bash array # for further manipulation by the run script. # # Maintenance notes: # An important requirement for the following implementation is that it works on macOS as well as on Linux. # Use https://www.gnu.org/software/gawk/manual/gawk.html as a reference for the AWK language but note # that it is a documentation for the GNU Awk (gawk) implementation that has some extra features (for example gensub()) # that are not available in awk found on macOS. # # DO NOT MODIFY OR SIMPLIFY THIS WITHOUT VERIFYING IT WORKS ON MACOS! awk ' function href(element) { match(element, /href="[^"]*"/) return substr(element, RSTART+6, RLENGTH-7) } function text(element, inner_text) { match(element, />[^<]+ "$region_file_csv" # read returns `false` here. Adding `|| true` allows the program to continue even with `set -e`. IFS=$'\n' read -d '' -r -a csv_lines < "$region_file_csv" || true IFS=';' read -r -a region_names <<< "${csv_lines[0]}" IFS=';' read -r -a region_sub_hrefs <<< "${csv_lines[1]}" IFS=';' read -r -a region_osm_hrefs <<< "${csv_lines[2]}" IFS=';' read -r -a region_sizes <<< "${csv_lines[3]}" # Make the array empty if it contains just 1 empty element. [[ ${#region_names[*]} == 1 && -z ${region_names[0]} ]] && region_names=() local -r max=$((${#region_names[*]} - 1)) if [[ ${max} -lt 0 ]] then echo echo "This region has no subregions to choose from." echo confirm "Do you want to download $region_osm_url?" && download "$region_osm_url" "$osm_dir/${region_osm_url##*/}" return 0 fi declare answer_region_id declare answer_action local -r format=" %2s %-30s %10s\n" local -r width=46 while true do echo # shellcheck disable=SC2059 printf "$format" "#" "REGION" "SIZE" printf "%.s=" $(seq 1 "$width") printf "\n" for i in "${!region_names[@]}" do # shellcheck disable=SC2059 printf "$format" "$i" "${region_names[$i]}" "${region_sizes[$i]}"; done read -r -p "Select a region (0-$max) or Enter to go back: " answer_region_id [[ -z ${answer_region_id} ]] && break if [[ ${answer_region_id} != [0-9] && ${answer_region_id} != [1-9][0-9] || ${answer_region_id} -gt ${max} ]] then echo "Wrong region ID '$answer_region_id'." continue fi read -r -p "Download (d) or enter (e): " answer_action if [[ ${answer_action} != [de] ]] then echo "Wrong action '$answer_action'." continue fi break done [[ -z ${answer_region_id} ]] && return 0 # osm_url is used either to download an OSM in the d) case or to pass it to next download_menu level # to make it possible to download it if there are no subregions to choose from in the next step. local -r osm_url=${url_parent}/${region_osm_hrefs[answer_region_id]} local -r subregion_html_url=${url_parent}/${region_sub_hrefs[answer_region_id]} case ${answer_action} in e) download_menu "$subregion_html_url" "$osm_url" ;; d) # Remove region prefix (e.g. europe/) from href to get the OSM file name. local -r osm_file=${osm_url##*/} local -r osm_target=${osm_dir}/${osm_file} if [[ -f ${osm_target} ]] then echo "Already downloaded." else download "$osm_url" "$osm_target" # Hack to set country code of any US state. if [[ ${osm_url}/ == */north-america/us/* ]] then echo "US" > "$cc_dir/${osm_file%.osm.pbf}" fi fi ;; *) echo "ERROR: Not possible (region_id=$answer_region_id,action=$answer_action)." exit 1 ;; esac } function interactive() { while true do IFS=$'\n' read -d '' -r -a regions <<< "$(for r in "$osm_dir"/* "$gh_dir"/*; do basename "$r" | sed 's/.osm.pbf//'; done | sort | uniq)" || true # Make the array empty if it contains just 1 empty element. [[ ${#regions[*]} == 1 && -z ${regions[0]} ]] && regions=() local format=" %2s %-24s %10s %10s %10s\n" local width=62 echo # shellcheck disable=SC2059 printf "$format" "#" "REGION" "OSM" "GRAPH" "COUNTRY" printf "%.s=" $(seq 1 "$width") printf "\n" local cc_status="OK" for i in "${!regions[@]}" do local region=${regions[$i]} # pass cc_error to skip repeated curl in this loop country_code "$region" "$cc_status" || cc_status="ERROR" # shellcheck disable=SC2059 printf "$format" \ "$i" \ "$region" \ "$(if [[ -f "$osm_dir/$region.osm.pbf" ]]; then echo "[x]"; else echo "[ ]"; fi)" \ "$(if [[ -d "$gh_dir/$region" ]]; then echo "[x]"; else echo "[ ]"; fi)" \ "$(cat "$cc_dir/$region")" done if [[ ${cc_status} == "ERROR" ]] then echo echo "ERROR: Failed to download country codes. Are you offline?" fi local max=$((${#regions[*]} - 1)) echo echo "Choose the next step:" echo "d: Download new region." echo "q: Quit." [[ ${max} -ge 0 ]] && echo "0-$max: Select a region and run OptaWeb Vehicle Routing." echo local command read -r -p "Your choice: " command case "$command" in q) exit 0 ;; d) download_menu "https://download.geofabrik.de/index.html" continue ;; [0-9] | [1-9][0-9]) if [[ ${command} -gt ${max} ]] then echo "Wrong number: $command" continue fi osm_file=${regions[$command]}.osm.pbf cc_list=$(cat "$cc_dir/${regions[$command]}") break ;; *) echo "Wrong command." continue ;; esac done echo "Region: $osm_file" echo "Country code list: $cc_list" echo confirm "Do you want to launch OptaWeb Vehicle Routing?" || abort standalone_jar_or_maven run_optaweb "$@" } function quickstart() { local url=https://download.geofabrik.de/europe/belgium-latest.osm.pbf osm_file="belgium-latest.osm.pbf" cc_list="BE" local -r osm_target=${osm_dir}/${osm_file} if [[ ! -f ${osm_target} ]] then echo "OptaWeb Vehicle Routing needs an OSM file for distance calculation. \ It contains a built-in dataset for $osm_file, which does not exist in $osm_dir. \ This script can download it for you from Geofabrik.de." confirm "Download $osm_file from Geofabrik.de now?" || abort download "$url" "$osm_target" echo "$cc_list" > "$cc_dir/${osm_file%.osm.pbf}" fi standalone_jar_or_maven run_optaweb "$@" } # Change dir to the project root (where the script is located). # This is needed to correctly resolve .VRP_DIR_LAST, path to the standalone JAR, etc. # in case the script was called from a different location than the project root. cd -P "$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" readonly last_vrp_dir_file=.DATA_DIR_LAST if [[ -f ${last_vrp_dir_file} ]] then readonly last_vrp_dir=$(cat ${last_vrp_dir_file}) else readonly last_vrp_dir="" fi if [[ -z ${last_vrp_dir} ]] then readonly vrp_dir=$HOME/.optaweb-vehicle-routing echo "There is no last used VRP dir. Using the default." else readonly vrp_dir=${last_vrp_dir} fi echo "VRP dir: $vrp_dir" if [[ ! -d ${vrp_dir} ]] then confirm "VRP dir '$vrp_dir' does not exist. Do you want to create it now?" || abort mkdir "${vrp_dir}" || { echo >&2 "Cannot create VRP directory '$vrp_dir'." exit 1 } fi # Remember VRP dir echo "${vrp_dir}" > ${last_vrp_dir_file} readonly error_log=${vrp_dir}/error.log rm -f ${error_log} readonly osm_dir=${vrp_dir}/openstreetmap readonly gh_dir=${vrp_dir}/graphhopper readonly cc_dir=${vrp_dir}/country_codes readonly dataset_dir=${vrp_dir}/dataset readonly cache_dir=${vrp_dir}/.cache readonly cache_geofabrik=${cache_dir}/geofabrik [[ -d ${osm_dir} ]] || mkdir "$osm_dir" [[ -d ${gh_dir} ]] || mkdir "$gh_dir" [[ -d ${cc_dir} ]] || mkdir "$cc_dir" [[ -d ${dataset_dir} ]] || mkdir "$dataset_dir" [[ -d ${cache_geofabrik} ]] || mkdir -p ${cache_geofabrik} declare routing_engine="GRAPHHOPPER" # Getting started (semi-interactive) - use OSM compatible with the built-in data set, download if not present. if [[ $# == 0 ]] then quickstart exit 0 fi # Use air mode (no OSM file, no country codes). if [[ $1 == "--air" ]] then routing_engine="AIR" shift echo >&2 "Air mode is currently not available. See https://github.com/kiegroup/optaweb-vehicle-routing/issues/455." exit 1 # standalone_jar_or_maven # run_optaweb "$@" # exit 0 fi case $1 in -i | --interactive) shift interactive "$@" ;; -*) quickstart "$@" ;; # Demo use case (non-interactive) - start with existing data. [a-z]*) region=${1%.osm.pbf} osm_file=${region}.osm.pbf if ! validate then region=${region}-latest osm_file=${region}.osm.pbf validate || { echo >&2 "Wrong region '$1'. One of the following must exist:" echo >&2 "- OSM file: $osm_dir/${region}.osm.pbf" echo >&2 "- GraphHopper graph: $gh_dir/$region" exit 1 } fi cc_list=$(cat "$cc_dir/$region") shift standalone_jar_or_maven run_optaweb "$@" ;; *) echo >&2 "Wrong argument." # TODO display help ;; esac ================================================ FILE: runOnOpenShift.sh ================================================ #!/usr/bin/env bash set -e readonly script_name="./$(basename "$0")" function print_help() { echo "Usage:" echo " $script_name [OSM_FILE_NAME COUNTRY_CODE_LIST OSM_FILE_DOWNLOAD_URL]" echo " $script_name --air" echo " $script_name --help" echo echo "First form configures the back end to use GraphHopper routing mode and downloads an OSM data file during startup. \ Note that download and processing of the OSM file can take some time depending on its size. \ During this period, the application informs about back end service being unreachable." echo echo "Second form configures back end to use air routing mode. This is useful for development, debugging and \ hacking. Air distance routing is only an approximation. It is not useful for real vehicle routing." echo echo echo "OSM_FILE_NAME" echo " The file downloaded from OSM_FILE_DOWNLOAD_URL will be saved under this name." echo echo "COUNTRY_CODE_LIST" echo " ISO_3166-1 country code used to filter geosearch results. You can provide multiple, comma-separated values." echo echo "OSM_FILE_DOWNLOAD_URL" echo " Should point to an OSM data file in PBF format accessible from OpenShift. The file will be downloaded \ during back end startup and saved as /deployments/local/OSM_FILE_NAME." echo echo echo "Example 1" echo " $script_name belgium-latest.osm.pbf BE https://download.geofabrik.de/europe/belgium-latest.osm.pbf" echo echo " Configures the application to filter geosearch results to Belgium and download the latest Belgium \ OSM extract from Geofabrik." echo echo echo "Example 2" echo " $script_name my-city.osm.pbf FR https://download.bbbike.org/osm/extract/planet_12.032,53.0171_12.1024,53.0491.osm.pbf" echo echo " Configures the application to download a custom region defined using the BBBike service and save it \ as my-city.osm.pbf." } function wrong_args() { print_help echo >&2 echo >&2 "ERROR: Wrong arguments." exit 1 } [[ $1 == "--help" ]] && print_help && exit 0 # Process arguments declare -a dc_backend_env case $# in 0) print_help exit 0 ;; 1) if [[ $1 == --air ]] then dc_backend_env+=("APP_ROUTING_ENGINE=AIR") summary="No routing config provided. The back end will start in air distance mode.\n\n\ WARNING: Air distance mode does not give accurate values. \ It is only useful for evaluation, debugging, or incremental setup purpose. \ You can run '$script_name --help' to see other options." else wrong_args fi ;; 2) dc_backend_env+=("APP_ROUTING_ENGINE=AIR") dc_backend_env+=("APP_ROUTING_OSM_FILE=$1") dc_backend_env+=("APP_REGION_COUNTRY_CODES=$2") summary="The back end will start in air mode. Use the back end pod to upload a graph directory or an OSM file. \ Then change routing mode to graphhopper. Run '$script_name --help' for more info." ;; 3) dc_backend_env+=("APP_ROUTING_ENGINE=GRAPHHOPPER") dc_backend_env+=("APP_ROUTING_OSM_FILE=$1") dc_backend_env+=("APP_REGION_COUNTRY_CODES=$2") dc_backend_env+=("APP_ROUTING_OSM_DOWNLOAD_URL=$3") summary="The back end will download an OSM file on startup. \ It may take several minutes to download and process the file before the application is fully available!" download=1 ;; *) wrong_args esac # Change dir to the project root (where the script is located) to correctly resolve module paths. # This is needed in case the script was called from a different location than the project root. cd -P "$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" readonly dir_backend=optaweb-vehicle-routing-backend readonly dir_frontend=optaweb-vehicle-routing-frontend # Fail fast if the project hasn't been built if ! stat -t ${dir_backend}/target/*.jar > /dev/null 2>&1 then echo >&2 "ERROR: The back end module is not built! Build the project before running this script." exit 1 fi if [[ ! -d ${dir_frontend}/docker/build ]] then echo >&2 "ERROR: The front end module is not built! Build the project before running this script." exit 1 fi command -v oc > /dev/null 2>&1 || { echo >&2 "ERROR: The oc client tool needs to be installed to connect to OpenShift." exit 1 } [[ -x $(command -v oc) ]] || { echo >&2 "ERROR: The oc client tool is not executable. Please make it executable by running \ 'chmod u+x \$(command -v oc)'." exit 1 } # Print info about the current user and project echo "Current user: $(oc whoami)" # Check that the current user has at least one project [[ -z "$(oc projects -q)" ]] && { echo >&2 "You have no projects. Use 'oc new-project ' to create one." exit 1 } # Display info about the current project oc project # Check that the current project is empty get_all=$(oc get all -o name) if [[ -z "$get_all" ]] then echo "The project appears to be empty." else echo >&2 echo >&2 "Project content:" echo >&2 echo >&2 "$get_all" echo >&2 echo >&2 "ERROR: The project is not empty." exit 1 fi echo echo -e "$summary" echo declare answer_continue read -r -p "Do you want to continue? [y/N]: " "answer_continue" [[ "$answer_continue" == "y" ]] || { echo "Aborted." exit 0 } # Set up PostgreSQL oc new-app --name postgresql postgresql-persistent # Back end # -- binary build (upload local artifacts + Dockerfile) oc new-build --name backend --strategy=docker --binary --build-arg QUARKUS_APP_BUILD_QUALIFIER=postgresql oc patch bc backend -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.jvm"}}}}' oc start-build backend --from-dir=${dir_backend} --follow # -- new app oc new-app backend # -- use PostgreSQL secret oc set env deployment/backend --from=secret/postgresql # -- set the rest of the configuration oc set env deployment/backend "${dc_backend_env[@]}" # Add a PersistentVolumeClaim oc set volumes deployment/backend --add \ --type pvc \ --claim-size 1Gi \ --claim-mode ReadWriteOnce \ --name data-local \ --mount-path /deployments/local # Front end # -- binary build oc new-build --name frontend --strategy=docker --binary oc start-build frontend --from-dir=${dir_frontend}/docker --follow # -- new app oc new-app frontend # -- expose the service oc expose svc/frontend # -- change target port to 8080 oc patch route frontend -p '{"spec":{"port":{"targetPort":"8080-tcp"}}}' echo echo "You can access the application at http://$(oc get route frontend -o custom-columns=:spec.host | tr -d '\n') \ once the deployment is done." if [[ -v download ]] then echo echo "The OSM file download and its processing can take some time depending on its size. \ For large files (hundreds of MB) this can be several minutes. \ During this period, the application informs about the back end service being unreachable." fi