From 2dc69659b605ab4ac42dc7387d3b26133be5a4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Erik=20St=C3=B8wer?= Date: Thu, 14 Dec 2023 16:20:23 +0100 Subject: [PATCH 01/20] CircleCI config, release scripts, and pom.xml updates. --- .circleci/config.yml | 52 +++++ .circleci/prepare_release | 235 +++++++++++++++++++++++ .circleci/release | 84 ++++++++ .circleci/update_deployment_config | 46 +++++ .gitignore | 1 + pom.xml | 12 +- src/main/java/EnturUpdatePomVersion.java | 135 +++++++++++++ 7 files changed, 559 insertions(+), 6 deletions(-) create mode 100644 .circleci/config.yml create mode 100755 .circleci/prepare_release create mode 100755 .circleci/release create mode 100755 .circleci/update_deployment_config create mode 100644 src/main/java/EnturUpdatePomVersion.java diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..8931a1bb608 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +aliases: + - &jfrog-login + name: Rename jfrog environment variable for maven setting.xml + command: | + echo "export JFROG_USER=$ARTIFACTORY_USER" >> $BASH_ENV + echo "export JFROG_PASS=$ARTIFACTORY_PASSWORD" >> $BASH_ENV + - &post-build + name: Update deployment with OTP version + command: | + sudo apt-get update + sudo apt-get install libxml2-utils + chmod u+x .circleci/update_deployment_config + .circleci/update_deployment_config + +jobs: + build: + docker: + - image: cimg/openjdk:21.0-node + environment: + DEBIAN_FRONTEND: "noninteractive" + MAVEN_OPTS: -Xmx6G + resource_class: xlarge + steps: + - checkout + - restore_cache: + keys: + - dep-cache-{{ checksum "pom.xml" }} + # fallback to the most recent cache if there is no exact match for this pom.xml + - dep-cache- + - run: wget https://raw.githubusercontent.com/entur/circleci-toolbox-image-java11/master/tools/m2/settings.xml -O .circleci/settings.xml + - run: *jfrog-login + - run: mvn org.apache.maven.plugins:maven-dependency-plugin:3.1.0:go-offline -s .circleci/settings.xml + - save_cache: + paths: + - ~/.m2 + key: dep-cache-{{ checksum "pom.xml" }} + - run: mvn install -PprettierSkip org.apache.maven.plugins:maven-deploy-plugin:2.8.2:deploy -s .circleci/settings.xml -DaltDeploymentRepository=snapshots::default::https://entur2.jfrog.io/entur2/libs-release-local + - run: *post-build + +workflows: + version: 2 + release: + jobs: + - build: + name: build-release + context: global + filters: + branches: + only: + - otp2_entur_develop \ No newline at end of file diff --git a/.circleci/prepare_release b/.circleci/prepare_release new file mode 100755 index 00000000000..5b2ad0e23c4 --- /dev/null +++ b/.circleci/prepare_release @@ -0,0 +1,235 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ENTUR_DEVELOP=otp2_entur_develop +REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +STATUS_FILE=".prepare_release.tmp" +STATUS="" +DRY_RUN="" +OTP_BASE="" + +function main() { + setup "$@" + resumePreviousExecution + resetEnturDevelop + rebaseAndMergeExtBranch otp2_ext_config + rebaseAndMergeExtBranch otp2_ext_hack_sorlandsbanen + logSuccess +} + +function setup() { + if [[ $# -eq 2 && "$1" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + OTP_BASE="$2" + elif [[ $# -eq 1 ]] ; then + OTP_BASE="$1" + else + printHelp + exit 1 + fi + + echo "" + echo "Options: ${DRY_RUN}" + echo "Git base branch/commit: ${OTP_BASE}" + echo "Entur develop branch: ${ENTUR_DEVELOP}" + echo "Entur remote repo(pull/push): ${REMOTE_REPO}" + echo "" + + if git diff-index --quiet HEAD --; then + echo "" + echo "OK - No local changes, prepare to checkout '${ENTUR_DEVELOP}'" + echo "" + else + echo "" + echo "You have local modification, the script will abort. Nothing done!" + exit 2 + fi + + git fetch ${REMOTE_REPO} +} + +# This script create a status file '.prepare_release.tmp'. This file is used to resume the +# script in the same spot as where is left when the error occurred. This allow us to fix the +# problem (merge conflict or compile error) and re-run the script to complete the proses. +function resumePreviousExecution() { + readStatus + + if [[ -n "${STATUS}" ]] ; then + echo "" + echo "Resume: ${STATUS}?" + echo "" + echo " If all problems are resolved you may continue." + echo " Exit to clear status and start over." + echo "" + + ANSWER="" + while [[ ! "$ANSWER" =~ [yx] ]]; do + echo "Do you want to resume: [y:Yes, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function resetEnturDevelop() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## RESET '${ENTUR_DEVELOP}' TO '${OTP_BASE}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "Would you like to reset the '${ENTUR_DEVELOP}' to '${OTP_BASE}'? " + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${ENTUR_DEVELOP}'" + git checkout ${ENTUR_DEVELOP} + + echo "" + echo "Reset '${ENTUR_DEVELOP}' branch to '${OTP_BASE}' (hard)" + git reset --hard "${OTP_BASE}" + echo "" + fi +} + +function rebaseAndMergeExtBranch() { + EXT_BRANCH="$1" + EXT_STATUS_REBASE="Rebase '${EXT_BRANCH}'" + EXT_STATUS_COMPILE="Compile '${EXT_BRANCH}'" + + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## REBASE AND MERGE '${EXT_BRANCH}' INTO '${ENTUR_DEVELOP}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "You are about to rebase and merge '${EXT_BRANCH}' into '${ENTUR_DEVELOP}'. Any local" + echo "modification in the '${EXT_BRANCH}' will be lost." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${EXT_BRANCH}'" + git checkout "${EXT_BRANCH}" + + echo "" + echo "Reset to '${REMOTE_REPO}/${EXT_BRANCH}'" + git reset --hard "${REMOTE_REPO}/${EXT_BRANCH}" + + echo "" + echo "Top 2 commits in '${EXT_BRANCH}'" + echo "-------------------------------------------------------------------------------------------" + git --no-pager log -2 + echo "-------------------------------------------------------------------------------------------" + echo "" + echo "You are about to rebase the TOP COMMIT ONLY(see above). Check that the " + echo "'${EXT_BRANCH}' only have ONE commit that you want to keep." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Rebase '${EXT_BRANCH}' onto '${ENTUR_DEVELOP}'" + setStatus "${EXT_STATUS_REBASE}" + git rebase --onto ${ENTUR_DEVELOP} HEAD~1 + fi + fi + + if [[ "${STATUS}" == "${EXT_STATUS_REBASE}" || "${STATUS}" == "${EXT_STATUS_COMPILE}" ]] ; then + # Reset status in case the test-compile fails. We need to do this because the status file + # is deleted after reading the status in the setup() function. + setStatus "${EXT_STATUS_COMPILE}" + + mvn clean test-compile + clearStatus + + echo "" + echo "Push '${EXT_BRANCH}'" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f + else + echo "Skip: git push -f (--dryRun)" + fi + + echo "" + echo "Checkout '${ENTUR_DEVELOP}' and merge in '${EXT_BRANCH}'" + git checkout "${ENTUR_DEVELOP}" + git merge "${EXT_BRANCH}" + fi +} + +function logSuccess() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## PREPARE RELEASE DONE -- SUCCESS" + echo "## ------------------------------------------------------------------------------------- ##" + echo " - '${REMOTE_REPO}/${ENTUR_DEVELOP}' reset to '${OTP_BASE}'" + echo " - 'otp2_ext_config' merged" + echo " - 'otp2_ext_hack_sorlandsbanen' merged" + echo "" + echo "" +} + +function whatDoYouWant() { + echo "" + ANSWER="" + + if [[ -n "${STATUS}" ]] ; then + # Skip until process is resumed + ANSWER="s" + else + while [[ ! "$ANSWER" =~ [ysx] ]]; do + echo "Do you want to continue: [y:Yes, s:Skip, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function setStatus() { + STATUS="$1" + echo "$STATUS" > "${STATUS_FILE}" +} + +function readStatus() { + if [[ -f "${STATUS_FILE}" ]] ; then + STATUS=`cat $STATUS_FILE` + rm "$STATUS_FILE" + else + STATUS="" + fi +} + +function clearStatus() { + STATUS="" + rm "${STATUS_FILE}" +} + +function printHelp() { + echo "" + echo "This script take ONE argument , the base **branch** or **commit** to use for the" + echo "release. The '${ENTUR_DEVELOP}' branch is reset to this commit and then the extension" + echo "branches is rebased onto that. The 'release' script is used to complete the release." + echo "It tag and push all changes to remote git repo." + echo "" + echo "Options:" + echo " --dryRun : Run script locally, nothing is pushed to remote server." + echo "" + echo "Usage:" + echo " $ .circleci/prepare_release otp/dev-2.x" + echo " $ .circleci/prepare_release --dryRun otp/dev-2.x" + echo "" +} + +main "$@" diff --git a/.circleci/release b/.circleci/release new file mode 100755 index 00000000000..9cac0d97958 --- /dev/null +++ b/.circleci/release @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +GIT_REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +MASTER_BRANCH=otp2_entur_develop +TAGS_FILE=target/git-entur-tags.txt +DRY_RUN="" + +function main() { + setup "$@" + listAllTags + mergeInOldReleaseWithNoChanges + setPomVersion + tagRelease + pushToRemote +} + +function setup() { + echo "" + echo "git fetch ${GIT_REMOTE_REPO}" + git fetch ${GIT_REMOTE_REPO} + + echo "Verify current branch is ${MASTER_BRANCH} " + git status | grep -q "On branch ${MASTER_BRANCH}" + + if [[ "${1+x}" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + fi +} + +function listAllTags() { + ## List all Entur tags to allow the UpdatePomVersion java program find the next version number + echo "" + echo "Dump all entur tags to ${TAGS_FILE}" + git tag -l | grep entur > ${TAGS_FILE} +} + +function setPomVersion() { + echo "" + echo "Update pom.xml with new version" + javac -d target/classes src/main/java/EnturUpdatePomVersion.java + + VERSION="`java -cp target/classes EnturUpdatePomVersion ${TAGS_FILE}`" + echo "" + echo "New version set: ${VERSION}" + echo "" + + ## Verify everything builds and tests run + echo "" + mvn clean test + + ## Add [ci skip] here before moving this to the CI server + echo "" + echo "Add and commit pom.xml" + git commit -m "Version ${VERSION}" pom.xml +} + +function mergeInOldReleaseWithNoChanges() { + echo "" + echo "Merge the old version of '${GIT_REMOTE_REPO}' into the new version. This only keep " + echo "a reference to the old version, the resulting tree of the merge is that of the new" + echo "branch head, effectively ignoring all changes from the old release." + git merge -s ours "${GIT_REMOTE_REPO}/${MASTER_BRANCH}" -m "Merge old release into '${MASTER_BRANCH}' - NO CHANGES COPIED OVER" +} + + +function tagRelease() { + echo "" + echo "Tag version ${VERSION}" + git tag -a v${VERSION} -m "Version ${VERSION}" +} + +function pushToRemote() { + echo "" + echo "Push pom.xml and new tag" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} + else + echo "Skip: push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} (--dryRun)" + fi +} + +main "$@" diff --git a/.circleci/update_deployment_config b/.circleci/update_deployment_config new file mode 100755 index 00000000000..64550f19797 --- /dev/null +++ b/.circleci/update_deployment_config @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## +## This script run AFTER OTP is build on the CI Server. It checkout the +## deployment config and update the version number, commit and push. This +## trigger the deployment config build witch create and deploy a new OTP2 +## docker image. +## +## Note! There is no need to run this script locally, unless you want to debug it. +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## + + +set -euo pipefail + +OTP_DEPLOYMENT_CONFIG=target/otp-deployment-config +DOCKER_FILE=Dockerfile + +VERSION=$(xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml) + +echo "Checkout otp-deplyment-config in ${OTP_DEPLOYMENT_CONFIG}" +git clone -n https://github.com/entur/otp-deployment-config.git --depth 1 ${OTP_DEPLOYMENT_CONFIG} + +pushd ${OTP_DEPLOYMENT_CONFIG} + +echo "Checkout latest version of master Dockerfile" +git checkout master ${DOCKER_FILE} + +echo "Update OTP version number in ${DOCKER_FILE}" +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -E -i '' "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +else + sed -E -i "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +fi + +if [[ "$CIRCLE_USERNAME" != "" ]]; then + git config user.email "circleci@entur.no" + git config user.name "circleci ($CIRCLE_USERNAME)" +fi + +echo "Add and commit Dockerfile" +git commit -m "New OTP2 Version ${VERSION}" ${DOCKER_FILE} + +echo "Push otp-deployment-config to GitHub" +git push + +popd \ No newline at end of file diff --git a/.gitignore b/.gitignore index a90f06cdcb0..3bc96654225 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .project .pydevproject .settings +.prepare_release.tmp .sonar .nvmrc *~ diff --git a/pom.xml b/pom.xml index c06bfcafeb1..b4e01ac71af 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 150 + EN-0068 31.1 2.51.1 @@ -84,11 +84,11 @@ - - github - OpenTripPlanner Maven Repository on Github Packages - https://maven.pkg.github.com/${GITHUB_REPOSITORY}/ - + + snapshots + entur2-snapshots + https://entur2.jfrog.io/entur2/libs-snapshot-local + diff --git a/src/main/java/EnturUpdatePomVersion.java b/src/main/java/EnturUpdatePomVersion.java new file mode 100644 index 00000000000..97bd72b3c6e --- /dev/null +++ b/src/main/java/EnturUpdatePomVersion.java @@ -0,0 +1,135 @@ +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +public class EnturUpdatePomVersion { + + private static final String VERSION_SEP = "-"; + private static final String ENTUR_PREFIX = "entur" + VERSION_SEP; + private static final Path POM_FILE = Path.of("pom.xml"); + + private final List tags = new ArrayList<>(); + private final List pomFile = new ArrayList<>(); + private String mainVersion; + private int versionNumber = 0; + + public static void main(String[] args) { + try { + new EnturUpdatePomVersion().withArgs(args).run(); + } catch (Exception e) { + System.err.println(e.getMessage()); + e.printStackTrace(System.err); + System.exit(10); + } + } + + private EnturUpdatePomVersion withArgs(String[] args) throws IOException { + if (args.length != 1 || args[0].matches("(?i)-h|--help")) { + printHelp(); + System.exit(1); + } + String arg = args[0]; + + if (arg.matches("\\d+")) { + versionNumber = resolveVersionFromNumericString(arg); + } else { + tags.addAll(readTagsFromFile(arg)); + } + return this; + } + + private void run() throws IOException { + readAndReplaceVersion(); + replacePomFile(); + } + + public void readAndReplaceVersion() throws IOException { + var pattern = Pattern.compile( + "(\\s*)(\\d+.\\d+.\\d+)" + + VERSION_SEP + + "(" + + ENTUR_PREFIX + + "\\d+|SNAPSHOT)(\\s*)" + ); + boolean found = false; + int i = 0; + + for (String line : Files.readAllLines(POM_FILE, UTF_8)) { + // Look for the version in the 25 first lines + if (!found) { + var m = pattern.matcher(line); + if (m.matches()) { + mainVersion = m.group(2); + String newVersion = mainVersion + VERSION_SEP + ENTUR_PREFIX + resolveVersionNumber(); + line = m.group(1) + newVersion + m.group(4); + System.out.println(newVersion); + found = true; + } + if (++i == 25) { + throw new IllegalStateException( + "Version not found in first 25 lines of the pom.xml file." + ); + } + } + pomFile.add(line); + } + if (!found) { + throw new IllegalStateException( + "Version not found in 'pom.xml'. Nothing matching pattern: " + pattern + ); + } + } + + public void replacePomFile() throws IOException { + Files.delete(POM_FILE); + Files.write(POM_FILE, pomFile, UTF_8); + } + + private static void printHelp() { + System.err.println( + "Use this small program to replace the OTP version '2.1.0-SNAPSHOT' \n" + + "with a new version number with a Entur qualifier like '2.1.0-entur-1'.\n" + + "\n" + + "Usage:\n" + + " $ java -cp .circleci UpdatePomVersion \n" + ); + } + + private int resolveVersionNumber() { + var pattern = Pattern.compile("v" + mainVersion + VERSION_SEP + ENTUR_PREFIX + "(\\d+)"); + int maxTagVersion = tags + .stream() + .mapToInt(tag -> { + var m = pattern.matcher(tag); + return m.matches() ? Integer.parseInt(m.group(1)) : -999; + }) + .max() + .orElse(-999); + + return 1 + Math.max(maxTagVersion, versionNumber); + } + + public static int resolveVersionFromNumericString(String arg) { + try { + return Integer.parseInt(arg); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Unable to parse input, decimal number expected: '" + arg + "'" + ); + } + } + + private static Collection readTagsFromFile(String arg) throws IOException { + var tags = Files.readAllLines(Path.of(arg)); + if (tags.isEmpty()) { + throw new IllegalStateException("Unable to load git tags from file: " + arg); + } + return tags; + } +} From 411d5b1bff75aa85e6cc22d283e750428734659f Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 19 Dec 2023 21:26:49 +0100 Subject: [PATCH 02/20] =?UTF-8?q?hack:=20Add=20train=20option=20for=20S?= =?UTF-8?q?=C3=B8rlandsbanen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Configuration.md | 1 + docs/RouteRequest.md | 1 + .../resources/RequestToPreferencesMapper.java | 1 + .../restapi/resources/RoutingResource.java | 3 + .../ConcurrentCompositeWorker.java | 53 +++++++ .../sorlandsbanen/EnturHackSorlandsBanen.java | 141 ++++++++++++++++++ .../ext/sorlandsbanen/PathKey.java | 51 +++++++ .../RaptorWorkerResultComposite.java | 88 +++++++++++ .../apis/transmodel/model/plan/TripQuery.java | 8 + .../framework/application/OTPFeature.java | 1 + .../raptor/api/request/RaptorRequest.java | 4 + .../api/request/RaptorRequestBuilder.java | 6 + .../service/RangeRaptorDynamicSearch.java | 10 +- .../raptoradapter/router/TransitRouter.java | 3 +- .../transit/cost/DefaultCostCalculator.java | 16 ++ .../transit/cost/FactorStrategy.java | 2 +- .../cost/IndexBasedFactorStrategy.java | 4 +- .../transit/mappers/RaptorRequestMapper.java | 17 ++- .../RaptorRoutingRequestTransitData.java | 33 ++++ .../preference/TransitPreferences.java | 17 +++ .../routerequest/RouteRequestConfig.java | 30 ++-- .../apis/transmodel/schema.graphql | 2 + .../raptor/RaptorArchitectureTest.java | 6 +- .../mappers/RaptorRequestMapperTest.java | 1 + 24 files changed, 478 insertions(+), 21 deletions(-) create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java diff --git a/docs/Configuration.md b/docs/Configuration.md index ed58f13fa6e..ee324f651ff 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -227,6 +227,7 @@ Here is a list of all features which can be toggled on/off and their default val | `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | | `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature if](sandbox/MapboxVectorTilesApi.md) you want a stable map tiles API. | ✓️ | | | `FloatingBike` | Enable floating bike routing. | ✓️ | | +| `HackSorlandsbanen` | Includ Sørlandsbanen | | ✓️ | | `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | | `GtfsGraphQlApiRentalStationFuzzyMatching` | Does vehicleRentalStation query also allow ids that are not feed scoped. | | | | `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | diff --git a/docs/RouteRequest.md b/docs/RouteRequest.md index 52fd177fbf6..fd3b9b4dc26 100644 --- a/docs/RouteRequest.md +++ b/docs/RouteRequest.md @@ -23,6 +23,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe | elevatorBoardTime | `integer` | How long does it take to get on an elevator, on average. | *Optional* | `90` | 2.0 | | elevatorHopCost | `integer` | What is the cost of travelling one floor on an elevator? | *Optional* | `20` | 2.0 | | elevatorHopTime | `integer` | How long does it take to advance one floor on an elevator? | *Optional* | `20` | 2.0 | +| extraSearchCoachReluctance | `double` | TODO | *Optional* | `0.0` | 2.1 | | geoidElevation | `boolean` | If true, the Graph's ellipsoidToGeoidDifference is applied to all elevations returned by this query. | *Optional* | `false` | 2.0 | | ignoreRealtimeUpdates | `boolean` | When true, real-time updates are ignored during this search. | *Optional* | `false` | 2.0 | | [intersectionTraversalModel](#rd_intersectionTraversalModel) | `enum` | The model that computes the costs of turns. | *Optional* | `"simple"` | 2.2 | diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java index 6248f71e214..f178af9abc0 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java @@ -132,6 +132,7 @@ private BoardAndAlightSlack mapTransit() { v -> tr.withRaptor(r -> r.withRelaxGeneralizedCostAtDestination(v)) ); } + setIfNotNull(req.extraSearchCoachReluctance, tr::setExtraSearchCoachReluctance); }); return new BoardAndAlightSlack( diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java index 1d985f0e555..7e0f8ea1dab 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java @@ -199,6 +199,9 @@ public abstract class RoutingResource { @QueryParam("carReluctance") protected Double carReluctance; + @QueryParam("extraSearchCoachReluctance") + protected Double extraSearchCoachReluctance; + /** * How much worse is waiting for a transit vehicle than being on a transit vehicle, as a * multiplier. The default value treats wait and on-vehicle time as the same. diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java new file mode 100644 index 00000000000..e80e106ce1e --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java @@ -0,0 +1,53 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.api.response.StopArrivals; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.multicriteria.McRaptorWorkerResult; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ConcurrentCompositeWorker implements RaptorWorker { + + private static final Logger LOG = LoggerFactory.getLogger(ConcurrentCompositeWorker.class); + + private final RaptorWorker mainWorker; + private final RaptorWorker alternativeWorker; + + ConcurrentCompositeWorker(RaptorWorker mainWorker, RaptorWorker alternativeWorker) { + this.mainWorker = mainWorker; + this.alternativeWorker = alternativeWorker; + } + + @Override + public RaptorWorkerResult route() { + if (OTPFeature.ParallelRouting.isOn()) { + var mainResultFuture = CompletableFuture.supplyAsync(mainWorker::route); + var alternativeResultFuture = CompletableFuture.supplyAsync(alternativeWorker::route); + + try { + return new RaptorWorkerResultComposite<>( + mainResultFuture.get(), + alternativeResultFuture.get() + ); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } else { + var mainResult = mainWorker.route(); + var alternativeResult = alternativeWorker.route(); + return new RaptorWorkerResultComposite<>(mainResult, alternativeResult); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java new file mode 100644 index 00000000000..9be309650aa --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java @@ -0,0 +1,141 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.function.Function; +import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.model.GenericLocation; +import org.opentripplanner.raptor.api.model.RaptorAccessEgress; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorRequest; +import org.opentripplanner.raptor.api.request.SearchParams; +import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; +import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.FactorStrategy; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.IndexBasedFactorStrategy; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; + +public class EnturHackSorlandsBanen { + + private static final double SOUTH_BOARDER_LIMIT = 59.1; + private static final int MIN_DISTANCE_LIMIT = 120_000; + + public static boolean match(RaptorRequest mcRequest) { + return mcRequest.extraSearchCoachReluctance > 0.1; + } + + public static RaptorWorker worker( + RaptorConfig config, + RaptorTransitDataProvider transitData, + RaptorRequest mcRequest, + Heuristics destinationHeuristics + ) { + //noinspection unchecked + RaptorTransitDataProvider altTransitData = (RaptorTransitDataProvider) ( + (RaptorRoutingRequestTransitData) transitData + ).enturHackSorlandsbanen(mapFactors(mcRequest.extraSearchCoachReluctance)); + + return new ConcurrentCompositeWorker<>( + config.createMcWorker(transitData, mcRequest, destinationHeuristics), + config.createMcWorker(altTransitData, mcRequest, destinationHeuristics) + ); + } + + public static RaptorRequest enableHack( + RaptorRequest raptorRequest, + RouteRequest request, + TransitLayer transitLayer + ) { + if (request.preferences().transit().extraSearchCoachReluctance() < 0.1) { + return raptorRequest; + } + + SearchParams params = raptorRequest.searchParams(); + + WgsCoordinate from = findStopCoordinate(request.from(), params.accessPaths(), transitLayer); + WgsCoordinate to = findStopCoordinate(request.to(), params.egressPaths(), transitLayer); + + if (from.latitude() > SOUTH_BOARDER_LIMIT && to.latitude() > SOUTH_BOARDER_LIMIT) { + return raptorRequest; + } + + double distanceMeters = SphericalDistanceLibrary.distance( + from.latitude(), + from.longitude(), + to.latitude(), + to.longitude() + ); + + if (distanceMeters < MIN_DISTANCE_LIMIT) { + return raptorRequest; + } + + raptorRequest.extraSearchCoachReluctance = + request.preferences().transit().extraSearchCoachReluctance(); + return raptorRequest; + } + + /* private methods */ + + private static Function mapFactors( + final double extraSearchCoachReluctance + ) { + return (FactorStrategy originalFactors) -> { + int[] modeReluctance = new int[TransitMode.values().length]; + for (TransitMode mode : TransitMode.values()) { + int index = mode.ordinal(); + int originalFactor = originalFactors.factor(index); + modeReluctance[index] = + mode == TransitMode.COACH + ? (int) (extraSearchCoachReluctance * originalFactor + 0.5) + : originalFactor; + } + return new IndexBasedFactorStrategy(modeReluctance); + }; + } + + /** + * Find a coordinate matching the given location, in order: + * - First return the coordinate of the location if it exists. + * - Then loop through the access/egress stops and try to find the + * stop or station given by the location id, return the stop/station coordinate. + * - Return the fist stop in the access/egress list coordinate. + */ + @SuppressWarnings("ConstantConditions") + private static WgsCoordinate findStopCoordinate( + GenericLocation location, + Collection accessEgress, + TransitLayer transitLayer + ) { + if (location.lat != null) { + return new WgsCoordinate(location.lat, location.lng); + } + + StopLocation firstStop = null; + for (RaptorAccessEgress it : accessEgress) { + StopLocation stop = transitLayer.getStopByIndex(it.stop()); + if (stop.getId().equals(location.stopId)) { + return stop.getCoordinate(); + } + if (idIsParentStation(stop, location.stopId)) { + return stop.getParentStation().getCoordinate(); + } + if (firstStop == null) { + firstStop = stop; + } + } + return firstStop.getCoordinate(); + } + + private static boolean idIsParentStation(StopLocation stop, FeedScopedId pId) { + return stop.getParentStation() != null && stop.getParentStation().getId().equals(pId); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java new file mode 100644 index 00000000000..94267b3e7c2 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java @@ -0,0 +1,51 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; + +final class PathKey { + + private final int hash; + + PathKey(RaptorPath path) { + this.hash = hash(path); + } + + private static int hash(RaptorPath path) { + if (path == null) { + return 0; + } + int result = 1; + + PathLeg leg = path.accessLeg(); + + while (!leg.isEgressLeg()) { + result = 31 * result + leg.toStop(); + result = 31 * result + leg.toTime(); + + if (leg.isTransitLeg()) { + result = 31 * result + leg.asTransitLeg().trip().pattern().debugInfo().hashCode(); + } + leg = leg.nextLeg(); + } + result = 31 * result + leg.toTime(); + + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o.getClass() != PathKey.class) { + return false; + } + return hash == ((PathKey) o).hash; + } + + @Override + public int hashCode() { + return hash; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java new file mode 100644 index 00000000000..094e652fc4c --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java @@ -0,0 +1,88 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RaptorWorkerResultComposite + implements RaptorWorkerResult { + + private static final Logger LOG = LoggerFactory.getLogger(RaptorWorkerResultComposite.class); + + private RaptorWorkerResult mainResult; + private RaptorWorkerResult alternativeResult; + + public RaptorWorkerResultComposite( + RaptorWorkerResult mainResult, + RaptorWorkerResult alternativeResult + ) { + this.mainResult = mainResult; + this.alternativeResult = alternativeResult; + } + + @Override + public Collection> extractPaths() { + Map> paths = new HashMap<>(); + addAll(paths, mainResult.extractPaths()); + addExtraRail(paths, alternativeResult.extractPaths()); + return paths.values(); + } + + @Override + public SingleCriteriaStopArrivals extractBestOverallArrivals() { + return mainResult.extractBestOverallArrivals(); + } + + @Override + public SingleCriteriaStopArrivals extractBestTransitArrivals() { + return mainResult.extractBestTransitArrivals(); + } + + @Override + public SingleCriteriaStopArrivals extractBestNumberOfTransfers() { + return mainResult.extractBestNumberOfTransfers(); + } + + @Override + public boolean isDestinationReached() { + return mainResult.isDestinationReached(); + } + + private void addExtraRail(Map> map, Collection> paths) { + paths.forEach(p -> { + if (hasRail(p)) { + var v = map.put(new PathKey(p), p); + LOG.debug("Ex.Rail {} : {}", (v == null ? "ADD " : "SKIP"), p); + } else { + LOG.debug("Ex. NOT Rail : {}", p); + } + }); + } + + private void addAll(Map> map, Collection> paths) { + paths.forEach(p -> { + var v = map.put(new PathKey(p), p); + LOG.debug("Normal {} : {}", (v == null ? "ADD " : "SKIP"), p); + }); + } + + private static boolean hasRail(RaptorPath path) { + return path + .legStream() + .filter(PathLeg::isTransitLeg) + .anyMatch(leg -> { + var trip = (TripScheduleWithOffset) leg.asTransitLeg().trip(); + var mode = trip.getOriginalTripPattern().getMode(); + return mode == TransitMode.RAIL; + }); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java index bf34d9928e4..c96956bc6b9 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java @@ -599,6 +599,14 @@ public static GraphQLFieldDefinition create( ) .build() ) + .argument( + GraphQLArgument + .newArgument() + .name("extraSearchCoachReluctance") + .description("FOR TESTING ONLY") + .type(Scalars.GraphQLFloat) + .build() + ) .dataFetcher(environment -> new TransmodelGraphQLPlanner().plan(environment)) .build(); } diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 4ae5004cf6b..3395c7261dd 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -32,6 +32,7 @@ public enum OTPFeature { """ ), FloatingBike(true, false, "Enable floating bike routing."), + HackSorlandsbanen(false, true, "Includ Sørlandsbanen"), GtfsGraphQlApi(true, false, "Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md)."), GtfsGraphQlApiRentalStationFuzzyMatching( false, diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java index 010bff4bc08..c53f0f90ae5 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java @@ -29,6 +29,9 @@ public class RaptorRequest { private final DebugRequest debug; private final RaptorTimers performanceTimers; + // HACK SØRLANDSBANEN + public double extraSearchCoachReluctance = 0.0; + private RaptorRequest() { searchParams = SearchParams.defaults(); profile = RaptorProfile.MULTI_CRITERIA; @@ -49,6 +52,7 @@ private RaptorRequest() { this.multiCriteria = builder.multiCriteria(); this.performanceTimers = builder.performanceTimers(); this.debug = builder.debug().build(); + this.extraSearchCoachReluctance = builder.extraSearchCoachReluctance; verify(); } diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java index ed2aab3de20..7bb8b538843 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java @@ -34,6 +34,9 @@ public class RaptorRequestBuilder { // Performance monitoring private RaptorTimers performanceTimers; + /** HACK SØRLANDSBANEN */ + public double extraSearchCoachReluctance; + // Algorithm private RaptorProfile profile; @@ -60,6 +63,9 @@ public RaptorRequestBuilder() { // Debug this.debug = new DebugRequestBuilder(defaults.debug()); + + // HACK SØRLANDSBANEN + this.extraSearchCoachReluctance = defaults.extraSearchCoachReluctance; } public SearchParamsBuilder searchParams() { diff --git a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java index fb96b7a8724..2e50dc2042d 100644 --- a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java +++ b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java @@ -11,6 +11,8 @@ import java.util.concurrent.Future; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.opentripplanner.ext.sorlandsbanen.EnturHackSorlandsBanen; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.raptor.RaptorService; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; @@ -134,7 +136,13 @@ private RaptorResponse createAndRunDynamicRRWorker(RaptorRequest request) // Create worker if (request.profile().is(MULTI_CRITERIA)) { - raptorWorker = config.createMcWorker(transitData, request, getDestinationHeuristics()); + // HACK SØRLANDSBANEN + if (OTPFeature.HackSorlandsbanen.isOn() && EnturHackSorlandsBanen.match(request)) { + raptorWorker = + EnturHackSorlandsBanen.worker(config, transitData, request, getDestinationHeuristics()); + } else { + raptorWorker = config.createMcWorker(transitData, request, getDestinationHeuristics()); + } } else { raptorWorker = config.createStdWorker(transitData, request); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index 0a9e46d2fe3..ad2434f4b70 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -127,7 +127,8 @@ private TransitRouterResult route() { serverContext.raptorConfig().isMultiThreaded(), accessEgresses.getAccesses(), accessEgresses.getEgresses(), - serverContext.meterRegistry() + serverContext.meterRegistry(), + transitLayer ); // Route transit diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java index 43faa3f0c40..c3bde3e1cc7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java @@ -1,5 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost; +import java.util.function.Function; import javax.annotation.Nullable; import org.opentripplanner.model.transfer.TransferConstraint; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; @@ -230,4 +231,19 @@ private int boardingCostConstrainedTransfer( // fallback to regular transfer return boardingCostRegularTransfer(firstBoarding, prevArrivalTime, boardStop, boardTime); } + + /*-- HACK SØRLANDSBANEN :: BEGIN --*/ + + public DefaultCostCalculator( + DefaultCostCalculator original, + Function modeReluctanceMapper + ) { + this.boardCostOnly = original.boardCostOnly; + this.boardAndTransferCost = original.boardAndTransferCost; + this.waitFactor = original.waitFactor; + this.transferCostOnly = original.transferCostOnly; + this.transitFactors = modeReluctanceMapper.apply(original.transitFactors); + this.stopBoardAlightTransferCosts = original.stopBoardAlightTransferCosts; + } + /*-- HACK SØRLANDSBANEN :: END --*/ } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java index fba2a7cfe38..8a14c27566d 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java @@ -6,7 +6,7 @@ *

* The class and methods are {@code final} to help the JIT compiler optimize the use of this class. */ -interface FactorStrategy { +public interface FactorStrategy { /** * Return the factor for the given index. */ diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java index 152f199248c..4c66b5b452e 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java @@ -6,12 +6,12 @@ * This class keep a facto for each index and the minimum factor for fast retrieval during Raptor * search. */ -final class IndexBasedFactorStrategy implements FactorStrategy { +public final class IndexBasedFactorStrategy implements FactorStrategy { private final int[] factors; private final int minFactor; - private IndexBasedFactorStrategy(int[] factors) { + public IndexBasedFactorStrategy(int[] factors) { this.factors = factors; this.minFactor = findMinimumFactor(factors); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java index 879536fdcd0..17b96790a5f 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java @@ -7,6 +7,7 @@ import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; +import org.opentripplanner.ext.sorlandsbanen.EnturHackSorlandsBanen; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.raptor.api.model.GeneralizedCostRelaxFunction; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; @@ -20,6 +21,8 @@ import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; import org.opentripplanner.raptor.rangeraptor.SystemErrDebugLogger; import org.opentripplanner.routing.algorithm.raptoradapter.router.performance.PerformanceTimersForRaptor; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; import org.opentripplanner.routing.api.request.DebugEventType; @@ -36,13 +39,16 @@ public class RaptorRequestMapper { private final boolean isMultiThreadedEnbled; private final MeterRegistry meterRegistry; + private final TransitLayer transitLayer; + private RaptorRequestMapper( RouteRequest request, boolean isMultiThreaded, Collection accessPaths, Collection egressPaths, long transitSearchTimeZeroEpocSecond, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + TransitLayer transitLayer ) { this.request = request; this.isMultiThreadedEnbled = isMultiThreaded; @@ -50,6 +56,7 @@ private RaptorRequestMapper( this.egressPaths = egressPaths; this.transitSearchTimeZeroEpocSecond = transitSearchTimeZeroEpocSecond; this.meterRegistry = meterRegistry; + this.transitLayer = transitLayer; } public static RaptorRequest mapRequest( @@ -58,7 +65,8 @@ public static RaptorRequest mapRequest( boolean isMultiThreaded, Collection accessPaths, Collection egressPaths, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + TransitLayer transitLayer ) { return new RaptorRequestMapper( request, @@ -66,7 +74,8 @@ public static RaptorRequest mapRequest( accessPaths, egressPaths, transitSearchTimeZero.toEpochSecond(), - meterRegistry + meterRegistry, + transitLayer ) .doMap(); } @@ -176,7 +185,7 @@ private RaptorRequest doMap() { ); } - return builder.build(); + return EnturHackSorlandsBanen.enableHack(builder.build(), request, transitLayer); } private List mapPassThroughPoints() { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java index 8c310206a01..d4785dc6f5b 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java @@ -4,6 +4,7 @@ import java.util.BitSet; import java.util.Iterator; import java.util.List; +import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.opentripplanner.framework.application.OTPFeature; @@ -27,6 +28,8 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.constrainedtransfer.ConstrainedBoardingSearch; import org.opentripplanner.routing.algorithm.raptoradapter.transit.constrainedtransfer.ConstrainedTransfersForPatterns; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.CostCalculatorFactory; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.DefaultCostCalculator; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.FactorStrategy; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.GeneralizedCostParametersMapper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.transit.model.network.RoutingTripPattern; @@ -253,4 +256,34 @@ private PriorityGroupConfigurator createTransitGroupPriorityConfigurator(RouteRe transitRequest.priorityGroupsGlobal() ); } + + /*-- HACK SØRLANDSBANEN :: BEGIN --*/ + + private RaptorRoutingRequestTransitData( + RaptorRoutingRequestTransitData original, + Function mapFactors + ) { + this.transitLayer = original.transitLayer; + this.transitSearchTimeZero = original.transitSearchTimeZero; + this.activeTripPatternsPerStop = original.activeTripPatternsPerStop; + this.patternIndex = original.patternIndex; + this.transferIndex = original.transferIndex; + this.transferService = original.transferService; + this.constrainedTransfers = original.constrainedTransfers; + this.validTransitDataStartTime = original.validTransitDataStartTime; + this.validTransitDataEndTime = original.validTransitDataEndTime; + this.generalizedCostCalculator = + new DefaultCostCalculator<>( + (DefaultCostCalculator) original.generalizedCostCalculator, + mapFactors + ); + this.slackProvider = original.slackProvider(); + } + + public RaptorTransitDataProvider enturHackSorlandsbanen( + Function mapFactors + ) { + return new RaptorRoutingRequestTransitData(this, mapFactors); + } + /*-- HACK SØRLANDSBANEN :: END --*/ } diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java index 78f30277e72..94a4a458915 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java @@ -31,6 +31,7 @@ public final class TransitPreferences implements Serializable { private final boolean includePlannedCancellations; private final boolean includeRealtimeCancellations; private final RaptorPreferences raptor; + private final double extraSearchCoachReluctance; private TransitPreferences() { this.boardSlack = this.alightSlack = DurationForEnum.of(TransitMode.class).build(); @@ -42,6 +43,7 @@ private TransitPreferences() { this.includePlannedCancellations = false; this.includeRealtimeCancellations = false; this.raptor = RaptorPreferences.DEFAULT; + this.extraSearchCoachReluctance = 0.0; } private TransitPreferences(Builder builder) { @@ -55,6 +57,7 @@ private TransitPreferences(Builder builder) { this.includePlannedCancellations = builder.includePlannedCancellations; this.includeRealtimeCancellations = builder.includeRealtimeCancellations; this.raptor = requireNonNull(builder.raptor); + this.extraSearchCoachReluctance = builder.extraSearchCoachReluctance; } public static Builder of() { @@ -165,6 +168,11 @@ public RaptorPreferences raptor() { return raptor; } + /** Zero means turned off. HACK SØRLANDSBANEN */ + public double extraSearchCoachReluctance() { + return extraSearchCoachReluctance; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -180,6 +188,7 @@ public boolean equals(Object o) { ignoreRealtimeUpdates == that.ignoreRealtimeUpdates && includePlannedCancellations == that.includePlannedCancellations && includeRealtimeCancellations == that.includeRealtimeCancellations && + extraSearchCoachReluctance == that.extraSearchCoachReluctance && raptor.equals(that.raptor) ); } @@ -196,6 +205,7 @@ public int hashCode() { ignoreRealtimeUpdates, includePlannedCancellations, includeRealtimeCancellations, + extraSearchCoachReluctance, raptor ); } @@ -245,6 +255,7 @@ public static class Builder { private boolean includePlannedCancellations; private boolean includeRealtimeCancellations; private RaptorPreferences raptor; + private double extraSearchCoachReluctance; public Builder(TransitPreferences original) { this.original = original; @@ -258,6 +269,7 @@ public Builder(TransitPreferences original) { this.includePlannedCancellations = original.includePlannedCancellations; this.includeRealtimeCancellations = original.includeRealtimeCancellations; this.raptor = original.raptor; + this.extraSearchCoachReluctance = original.extraSearchCoachReluctance; } public TransitPreferences original() { @@ -327,6 +339,11 @@ public Builder withRaptor(Consumer body) { return this; } + public Builder setExtraSearchCoachReluctance(double extraSearchCoachReluctance) { + this.extraSearchCoachReluctance = extraSearchCoachReluctance; + return this; + } + public Builder apply(Consumer body) { body.accept(this); return this; diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index 628008da220..a73b3c26d76 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -317,13 +317,14 @@ The board time is added to the time when going from the stop (offboard) to onboa } // TODO REMOVE THIS - builder.withRaptor(it -> - c - .of("relaxTransitSearchGeneralizedCostAtDestination") - .since(V2_3) - .summary("Whether non-optimal transit paths at the destination should be returned") - .description( - """ + builder + .withRaptor(it -> + c + .of("relaxTransitSearchGeneralizedCostAtDestination") + .since(V2_3) + .summary("Whether non-optimal transit paths at the destination should be returned") + .description( + """ Let c be the existing minimum pareto optimal generalized cost to beat. Then a trip with cost c' is accepted if the following is true: `c' < Math.round(c * relaxRaptorCostCriteria)`. @@ -333,10 +334,17 @@ The board time is added to the time when going from the stop (offboard) to onboa Values equals or less than zero is not allowed. Values greater than 2.0 are not supported, due to performance reasons. """ - ) - .asDoubleOptional() - .ifPresent(it::withRelaxGeneralizedCostAtDestination) - ); + ) + .asDoubleOptional() + .ifPresent(it::withRelaxGeneralizedCostAtDestination) + ) + .setExtraSearchCoachReluctance( + c + .of("extraSearchCoachReluctance") + .since(V2_1) + .summary("TODO") + .asDouble(dft.extraSearchCoachReluctance()) + ); } private static void mapBikePreferences(NodeAdapter root, BikePreferences.Builder builder) { diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index e6f4208c643..fbda3226515 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -796,6 +796,8 @@ type QueryType { dateTime: DateTime, "Debug the itinerary-filter-chain. OTP will attach a system notice to itineraries instead of removing them. This is very convenient when tuning the filters." debugItineraryFilter: Boolean = false @deprecated(reason : "Use `itineraryFilter.debug` instead."), + "FOR TESTING ONLY" + extraSearchCoachReluctance: Float, "A list of filters for which trips should be included. A trip will be included if it matches with at least one filter. An empty list of filters means that all trips should be included. If a search include this parameter, \"whiteListed\", \"banned\" & \"modes.transportModes\" filters will be ignored." filters: [TripFilterInput!], "The start location" diff --git a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java index 39022da42ca..aeca5a36ebf 100644 --- a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java +++ b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java @@ -39,6 +39,9 @@ public class RaptorArchitectureTest { private static final Package RR_STANDARD = RANGE_RAPTOR.subPackage("standard"); private static final Package RR_STD_CONFIGURE = RR_STANDARD.subPackage("configure"); private static final Package RR_CONTEXT = RANGE_RAPTOR.subPackage("context"); + private static final Package EXT_SORLANDSBANAN_HACK = Package.of( + "org.opentripplanner.ext.sorlandsbanen" + ); /** * Packages used by standard-range-raptor and multi-criteria-range-raptor. @@ -200,7 +203,8 @@ void enforcePackageDependenciesInRaptorService() { RAPTOR_UTIL, CONFIGURE, RR_INTERNAL_API, - RR_TRANSIT + RR_TRANSIT, + EXT_SORLANDSBANAN_HACK ) .verify(); } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java index 47542782884..e3fbbc0df21 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java @@ -90,6 +90,7 @@ private static RaptorRequest map(RouteRequest request) { false, ACCESS, EGRESS, + null, null ); } From 281383234f0692a513ed7bb7324e5cb8f320e18c Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 31 May 2024 11:31:24 +0200 Subject: [PATCH 03/20] Fix spelling in doc --- .../org/opentripplanner/apis/transmodel/schema.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 4a6f462624f..1b01b2c2276 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -801,7 +801,7 @@ type QueryType { restrictions are applied - all journeys are listed. """ bookingTime: DateTime, - "The date and time for the earliest time the user is willing to start the journey (if `false`or not set) or the latest acceptable time of arriving (`true`). Defaults to now" + "The date and time for the earliest time the user is willing to start the journey (if `false` or not set) or the latest acceptable time of arriving (`true`). Defaults to now" dateTime: DateTime, "Debug the itinerary-filter-chain. OTP will attach a system notice to itineraries instead of removing them. This is very convenient when tuning the filters." debugItineraryFilter: Boolean = false @deprecated(reason : "Use `itineraryFilter.debug` instead."), From 9742fe617a715fd90d2705f595c0c4684cfefd0d Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 31 May 2024 11:32:47 +0200 Subject: [PATCH 04/20] Version 2.6.0-entur-11 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b4e01ac71af..a2ff4ae435c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-SNAPSHOT + 2.6.0-entur-11 jar From ab1f994093fd9e6acfc788677d697c1f9f477625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Erik=20St=C3=B8wer?= Date: Thu, 14 Dec 2023 16:20:23 +0100 Subject: [PATCH 05/20] CircleCI config, release scripts, and pom.xml updates. --- .circleci/config.yml | 52 +++++ .circleci/prepare_release | 235 +++++++++++++++++++++++ .circleci/release | 84 ++++++++ .circleci/update_deployment_config | 46 +++++ .gitignore | 1 + pom.xml | 12 +- src/main/java/EnturUpdatePomVersion.java | 135 +++++++++++++ 7 files changed, 559 insertions(+), 6 deletions(-) create mode 100644 .circleci/config.yml create mode 100755 .circleci/prepare_release create mode 100755 .circleci/release create mode 100755 .circleci/update_deployment_config create mode 100644 src/main/java/EnturUpdatePomVersion.java diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..8931a1bb608 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +aliases: + - &jfrog-login + name: Rename jfrog environment variable for maven setting.xml + command: | + echo "export JFROG_USER=$ARTIFACTORY_USER" >> $BASH_ENV + echo "export JFROG_PASS=$ARTIFACTORY_PASSWORD" >> $BASH_ENV + - &post-build + name: Update deployment with OTP version + command: | + sudo apt-get update + sudo apt-get install libxml2-utils + chmod u+x .circleci/update_deployment_config + .circleci/update_deployment_config + +jobs: + build: + docker: + - image: cimg/openjdk:21.0-node + environment: + DEBIAN_FRONTEND: "noninteractive" + MAVEN_OPTS: -Xmx6G + resource_class: xlarge + steps: + - checkout + - restore_cache: + keys: + - dep-cache-{{ checksum "pom.xml" }} + # fallback to the most recent cache if there is no exact match for this pom.xml + - dep-cache- + - run: wget https://raw.githubusercontent.com/entur/circleci-toolbox-image-java11/master/tools/m2/settings.xml -O .circleci/settings.xml + - run: *jfrog-login + - run: mvn org.apache.maven.plugins:maven-dependency-plugin:3.1.0:go-offline -s .circleci/settings.xml + - save_cache: + paths: + - ~/.m2 + key: dep-cache-{{ checksum "pom.xml" }} + - run: mvn install -PprettierSkip org.apache.maven.plugins:maven-deploy-plugin:2.8.2:deploy -s .circleci/settings.xml -DaltDeploymentRepository=snapshots::default::https://entur2.jfrog.io/entur2/libs-release-local + - run: *post-build + +workflows: + version: 2 + release: + jobs: + - build: + name: build-release + context: global + filters: + branches: + only: + - otp2_entur_develop \ No newline at end of file diff --git a/.circleci/prepare_release b/.circleci/prepare_release new file mode 100755 index 00000000000..5b2ad0e23c4 --- /dev/null +++ b/.circleci/prepare_release @@ -0,0 +1,235 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ENTUR_DEVELOP=otp2_entur_develop +REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +STATUS_FILE=".prepare_release.tmp" +STATUS="" +DRY_RUN="" +OTP_BASE="" + +function main() { + setup "$@" + resumePreviousExecution + resetEnturDevelop + rebaseAndMergeExtBranch otp2_ext_config + rebaseAndMergeExtBranch otp2_ext_hack_sorlandsbanen + logSuccess +} + +function setup() { + if [[ $# -eq 2 && "$1" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + OTP_BASE="$2" + elif [[ $# -eq 1 ]] ; then + OTP_BASE="$1" + else + printHelp + exit 1 + fi + + echo "" + echo "Options: ${DRY_RUN}" + echo "Git base branch/commit: ${OTP_BASE}" + echo "Entur develop branch: ${ENTUR_DEVELOP}" + echo "Entur remote repo(pull/push): ${REMOTE_REPO}" + echo "" + + if git diff-index --quiet HEAD --; then + echo "" + echo "OK - No local changes, prepare to checkout '${ENTUR_DEVELOP}'" + echo "" + else + echo "" + echo "You have local modification, the script will abort. Nothing done!" + exit 2 + fi + + git fetch ${REMOTE_REPO} +} + +# This script create a status file '.prepare_release.tmp'. This file is used to resume the +# script in the same spot as where is left when the error occurred. This allow us to fix the +# problem (merge conflict or compile error) and re-run the script to complete the proses. +function resumePreviousExecution() { + readStatus + + if [[ -n "${STATUS}" ]] ; then + echo "" + echo "Resume: ${STATUS}?" + echo "" + echo " If all problems are resolved you may continue." + echo " Exit to clear status and start over." + echo "" + + ANSWER="" + while [[ ! "$ANSWER" =~ [yx] ]]; do + echo "Do you want to resume: [y:Yes, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function resetEnturDevelop() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## RESET '${ENTUR_DEVELOP}' TO '${OTP_BASE}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "Would you like to reset the '${ENTUR_DEVELOP}' to '${OTP_BASE}'? " + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${ENTUR_DEVELOP}'" + git checkout ${ENTUR_DEVELOP} + + echo "" + echo "Reset '${ENTUR_DEVELOP}' branch to '${OTP_BASE}' (hard)" + git reset --hard "${OTP_BASE}" + echo "" + fi +} + +function rebaseAndMergeExtBranch() { + EXT_BRANCH="$1" + EXT_STATUS_REBASE="Rebase '${EXT_BRANCH}'" + EXT_STATUS_COMPILE="Compile '${EXT_BRANCH}'" + + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## REBASE AND MERGE '${EXT_BRANCH}' INTO '${ENTUR_DEVELOP}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "You are about to rebase and merge '${EXT_BRANCH}' into '${ENTUR_DEVELOP}'. Any local" + echo "modification in the '${EXT_BRANCH}' will be lost." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${EXT_BRANCH}'" + git checkout "${EXT_BRANCH}" + + echo "" + echo "Reset to '${REMOTE_REPO}/${EXT_BRANCH}'" + git reset --hard "${REMOTE_REPO}/${EXT_BRANCH}" + + echo "" + echo "Top 2 commits in '${EXT_BRANCH}'" + echo "-------------------------------------------------------------------------------------------" + git --no-pager log -2 + echo "-------------------------------------------------------------------------------------------" + echo "" + echo "You are about to rebase the TOP COMMIT ONLY(see above). Check that the " + echo "'${EXT_BRANCH}' only have ONE commit that you want to keep." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Rebase '${EXT_BRANCH}' onto '${ENTUR_DEVELOP}'" + setStatus "${EXT_STATUS_REBASE}" + git rebase --onto ${ENTUR_DEVELOP} HEAD~1 + fi + fi + + if [[ "${STATUS}" == "${EXT_STATUS_REBASE}" || "${STATUS}" == "${EXT_STATUS_COMPILE}" ]] ; then + # Reset status in case the test-compile fails. We need to do this because the status file + # is deleted after reading the status in the setup() function. + setStatus "${EXT_STATUS_COMPILE}" + + mvn clean test-compile + clearStatus + + echo "" + echo "Push '${EXT_BRANCH}'" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f + else + echo "Skip: git push -f (--dryRun)" + fi + + echo "" + echo "Checkout '${ENTUR_DEVELOP}' and merge in '${EXT_BRANCH}'" + git checkout "${ENTUR_DEVELOP}" + git merge "${EXT_BRANCH}" + fi +} + +function logSuccess() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## PREPARE RELEASE DONE -- SUCCESS" + echo "## ------------------------------------------------------------------------------------- ##" + echo " - '${REMOTE_REPO}/${ENTUR_DEVELOP}' reset to '${OTP_BASE}'" + echo " - 'otp2_ext_config' merged" + echo " - 'otp2_ext_hack_sorlandsbanen' merged" + echo "" + echo "" +} + +function whatDoYouWant() { + echo "" + ANSWER="" + + if [[ -n "${STATUS}" ]] ; then + # Skip until process is resumed + ANSWER="s" + else + while [[ ! "$ANSWER" =~ [ysx] ]]; do + echo "Do you want to continue: [y:Yes, s:Skip, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function setStatus() { + STATUS="$1" + echo "$STATUS" > "${STATUS_FILE}" +} + +function readStatus() { + if [[ -f "${STATUS_FILE}" ]] ; then + STATUS=`cat $STATUS_FILE` + rm "$STATUS_FILE" + else + STATUS="" + fi +} + +function clearStatus() { + STATUS="" + rm "${STATUS_FILE}" +} + +function printHelp() { + echo "" + echo "This script take ONE argument , the base **branch** or **commit** to use for the" + echo "release. The '${ENTUR_DEVELOP}' branch is reset to this commit and then the extension" + echo "branches is rebased onto that. The 'release' script is used to complete the release." + echo "It tag and push all changes to remote git repo." + echo "" + echo "Options:" + echo " --dryRun : Run script locally, nothing is pushed to remote server." + echo "" + echo "Usage:" + echo " $ .circleci/prepare_release otp/dev-2.x" + echo " $ .circleci/prepare_release --dryRun otp/dev-2.x" + echo "" +} + +main "$@" diff --git a/.circleci/release b/.circleci/release new file mode 100755 index 00000000000..9cac0d97958 --- /dev/null +++ b/.circleci/release @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +GIT_REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +MASTER_BRANCH=otp2_entur_develop +TAGS_FILE=target/git-entur-tags.txt +DRY_RUN="" + +function main() { + setup "$@" + listAllTags + mergeInOldReleaseWithNoChanges + setPomVersion + tagRelease + pushToRemote +} + +function setup() { + echo "" + echo "git fetch ${GIT_REMOTE_REPO}" + git fetch ${GIT_REMOTE_REPO} + + echo "Verify current branch is ${MASTER_BRANCH} " + git status | grep -q "On branch ${MASTER_BRANCH}" + + if [[ "${1+x}" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + fi +} + +function listAllTags() { + ## List all Entur tags to allow the UpdatePomVersion java program find the next version number + echo "" + echo "Dump all entur tags to ${TAGS_FILE}" + git tag -l | grep entur > ${TAGS_FILE} +} + +function setPomVersion() { + echo "" + echo "Update pom.xml with new version" + javac -d target/classes src/main/java/EnturUpdatePomVersion.java + + VERSION="`java -cp target/classes EnturUpdatePomVersion ${TAGS_FILE}`" + echo "" + echo "New version set: ${VERSION}" + echo "" + + ## Verify everything builds and tests run + echo "" + mvn clean test + + ## Add [ci skip] here before moving this to the CI server + echo "" + echo "Add and commit pom.xml" + git commit -m "Version ${VERSION}" pom.xml +} + +function mergeInOldReleaseWithNoChanges() { + echo "" + echo "Merge the old version of '${GIT_REMOTE_REPO}' into the new version. This only keep " + echo "a reference to the old version, the resulting tree of the merge is that of the new" + echo "branch head, effectively ignoring all changes from the old release." + git merge -s ours "${GIT_REMOTE_REPO}/${MASTER_BRANCH}" -m "Merge old release into '${MASTER_BRANCH}' - NO CHANGES COPIED OVER" +} + + +function tagRelease() { + echo "" + echo "Tag version ${VERSION}" + git tag -a v${VERSION} -m "Version ${VERSION}" +} + +function pushToRemote() { + echo "" + echo "Push pom.xml and new tag" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} + else + echo "Skip: push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} (--dryRun)" + fi +} + +main "$@" diff --git a/.circleci/update_deployment_config b/.circleci/update_deployment_config new file mode 100755 index 00000000000..64550f19797 --- /dev/null +++ b/.circleci/update_deployment_config @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## +## This script run AFTER OTP is build on the CI Server. It checkout the +## deployment config and update the version number, commit and push. This +## trigger the deployment config build witch create and deploy a new OTP2 +## docker image. +## +## Note! There is no need to run this script locally, unless you want to debug it. +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## + + +set -euo pipefail + +OTP_DEPLOYMENT_CONFIG=target/otp-deployment-config +DOCKER_FILE=Dockerfile + +VERSION=$(xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml) + +echo "Checkout otp-deplyment-config in ${OTP_DEPLOYMENT_CONFIG}" +git clone -n https://github.com/entur/otp-deployment-config.git --depth 1 ${OTP_DEPLOYMENT_CONFIG} + +pushd ${OTP_DEPLOYMENT_CONFIG} + +echo "Checkout latest version of master Dockerfile" +git checkout master ${DOCKER_FILE} + +echo "Update OTP version number in ${DOCKER_FILE}" +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -E -i '' "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +else + sed -E -i "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +fi + +if [[ "$CIRCLE_USERNAME" != "" ]]; then + git config user.email "circleci@entur.no" + git config user.name "circleci ($CIRCLE_USERNAME)" +fi + +echo "Add and commit Dockerfile" +git commit -m "New OTP2 Version ${VERSION}" ${DOCKER_FILE} + +echo "Push otp-deployment-config to GitHub" +git push + +popd \ No newline at end of file diff --git a/.gitignore b/.gitignore index a90f06cdcb0..3bc96654225 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .project .pydevproject .settings +.prepare_release.tmp .sonar .nvmrc *~ diff --git a/pom.xml b/pom.xml index 5880d9dcc62..585b93f8848 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 150 + EN-0067 31.1 2.51.1 @@ -84,11 +84,11 @@ - - github - OpenTripPlanner Maven Repository on Github Packages - https://maven.pkg.github.com/${GITHUB_REPOSITORY}/ - + + snapshots + entur2-snapshots + https://entur2.jfrog.io/entur2/libs-snapshot-local + diff --git a/src/main/java/EnturUpdatePomVersion.java b/src/main/java/EnturUpdatePomVersion.java new file mode 100644 index 00000000000..97bd72b3c6e --- /dev/null +++ b/src/main/java/EnturUpdatePomVersion.java @@ -0,0 +1,135 @@ +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +public class EnturUpdatePomVersion { + + private static final String VERSION_SEP = "-"; + private static final String ENTUR_PREFIX = "entur" + VERSION_SEP; + private static final Path POM_FILE = Path.of("pom.xml"); + + private final List tags = new ArrayList<>(); + private final List pomFile = new ArrayList<>(); + private String mainVersion; + private int versionNumber = 0; + + public static void main(String[] args) { + try { + new EnturUpdatePomVersion().withArgs(args).run(); + } catch (Exception e) { + System.err.println(e.getMessage()); + e.printStackTrace(System.err); + System.exit(10); + } + } + + private EnturUpdatePomVersion withArgs(String[] args) throws IOException { + if (args.length != 1 || args[0].matches("(?i)-h|--help")) { + printHelp(); + System.exit(1); + } + String arg = args[0]; + + if (arg.matches("\\d+")) { + versionNumber = resolveVersionFromNumericString(arg); + } else { + tags.addAll(readTagsFromFile(arg)); + } + return this; + } + + private void run() throws IOException { + readAndReplaceVersion(); + replacePomFile(); + } + + public void readAndReplaceVersion() throws IOException { + var pattern = Pattern.compile( + "(\\s*)(\\d+.\\d+.\\d+)" + + VERSION_SEP + + "(" + + ENTUR_PREFIX + + "\\d+|SNAPSHOT)(\\s*)" + ); + boolean found = false; + int i = 0; + + for (String line : Files.readAllLines(POM_FILE, UTF_8)) { + // Look for the version in the 25 first lines + if (!found) { + var m = pattern.matcher(line); + if (m.matches()) { + mainVersion = m.group(2); + String newVersion = mainVersion + VERSION_SEP + ENTUR_PREFIX + resolveVersionNumber(); + line = m.group(1) + newVersion + m.group(4); + System.out.println(newVersion); + found = true; + } + if (++i == 25) { + throw new IllegalStateException( + "Version not found in first 25 lines of the pom.xml file." + ); + } + } + pomFile.add(line); + } + if (!found) { + throw new IllegalStateException( + "Version not found in 'pom.xml'. Nothing matching pattern: " + pattern + ); + } + } + + public void replacePomFile() throws IOException { + Files.delete(POM_FILE); + Files.write(POM_FILE, pomFile, UTF_8); + } + + private static void printHelp() { + System.err.println( + "Use this small program to replace the OTP version '2.1.0-SNAPSHOT' \n" + + "with a new version number with a Entur qualifier like '2.1.0-entur-1'.\n" + + "\n" + + "Usage:\n" + + " $ java -cp .circleci UpdatePomVersion \n" + ); + } + + private int resolveVersionNumber() { + var pattern = Pattern.compile("v" + mainVersion + VERSION_SEP + ENTUR_PREFIX + "(\\d+)"); + int maxTagVersion = tags + .stream() + .mapToInt(tag -> { + var m = pattern.matcher(tag); + return m.matches() ? Integer.parseInt(m.group(1)) : -999; + }) + .max() + .orElse(-999); + + return 1 + Math.max(maxTagVersion, versionNumber); + } + + public static int resolveVersionFromNumericString(String arg) { + try { + return Integer.parseInt(arg); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Unable to parse input, decimal number expected: '" + arg + "'" + ); + } + } + + private static Collection readTagsFromFile(String arg) throws IOException { + var tags = Files.readAllLines(Path.of(arg)); + if (tags.isEmpty()) { + throw new IllegalStateException("Unable to load git tags from file: " + arg); + } + return tags; + } +} From 8fbef65d210f700091c858c691c8d4e1baa84aa5 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 19 Dec 2023 21:26:49 +0100 Subject: [PATCH 06/20] =?UTF-8?q?hack:=20Add=20train=20option=20for=20S?= =?UTF-8?q?=C3=B8rlandsbanen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Configuration.md | 1 + docs/RouteRequest.md | 1 + .../resources/RequestToPreferencesMapper.java | 1 + .../restapi/resources/RoutingResource.java | 3 + .../ConcurrentCompositeWorker.java | 53 +++++++ .../sorlandsbanen/EnturHackSorlandsBanen.java | 141 ++++++++++++++++++ .../ext/sorlandsbanen/PathKey.java | 51 +++++++ .../RaptorWorkerResultComposite.java | 88 +++++++++++ .../apis/transmodel/model/plan/TripQuery.java | 8 + .../framework/application/OTPFeature.java | 1 + .../raptor/api/request/RaptorRequest.java | 4 + .../api/request/RaptorRequestBuilder.java | 6 + .../service/RangeRaptorDynamicSearch.java | 10 +- .../raptoradapter/router/TransitRouter.java | 3 +- .../transit/cost/DefaultCostCalculator.java | 16 ++ .../transit/cost/FactorStrategy.java | 2 +- .../cost/IndexBasedFactorStrategy.java | 4 +- .../transit/mappers/RaptorRequestMapper.java | 17 ++- .../RaptorRoutingRequestTransitData.java | 33 ++++ .../preference/TransitPreferences.java | 17 +++ .../routerequest/RouteRequestConfig.java | 30 ++-- .../apis/transmodel/schema.graphql | 2 + .../raptor/RaptorArchitectureTest.java | 6 +- .../mappers/RaptorRequestMapperTest.java | 1 + 24 files changed, 478 insertions(+), 21 deletions(-) create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java diff --git a/docs/Configuration.md b/docs/Configuration.md index ed58f13fa6e..ee324f651ff 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -227,6 +227,7 @@ Here is a list of all features which can be toggled on/off and their default val | `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | | `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature if](sandbox/MapboxVectorTilesApi.md) you want a stable map tiles API. | ✓️ | | | `FloatingBike` | Enable floating bike routing. | ✓️ | | +| `HackSorlandsbanen` | Includ Sørlandsbanen | | ✓️ | | `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | | `GtfsGraphQlApiRentalStationFuzzyMatching` | Does vehicleRentalStation query also allow ids that are not feed scoped. | | | | `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | diff --git a/docs/RouteRequest.md b/docs/RouteRequest.md index 0fd9ec47f89..790984ac02c 100644 --- a/docs/RouteRequest.md +++ b/docs/RouteRequest.md @@ -23,6 +23,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe | elevatorBoardTime | `integer` | How long does it take to get on an elevator, on average. | *Optional* | `90` | 2.0 | | elevatorHopCost | `integer` | What is the cost of travelling one floor on an elevator? | *Optional* | `20` | 2.0 | | elevatorHopTime | `integer` | How long does it take to advance one floor on an elevator? | *Optional* | `20` | 2.0 | +| extraSearchCoachReluctance | `double` | TODO | *Optional* | `0.0` | 2.1 | | geoidElevation | `boolean` | If true, the Graph's ellipsoidToGeoidDifference is applied to all elevations returned by this query. | *Optional* | `false` | 2.0 | | ignoreRealtimeUpdates | `boolean` | When true, real-time updates are ignored during this search. | *Optional* | `false` | 2.0 | | [intersectionTraversalModel](#rd_intersectionTraversalModel) | `enum` | The model that computes the costs of turns. | *Optional* | `"simple"` | 2.2 | diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java index 6248f71e214..f178af9abc0 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java @@ -132,6 +132,7 @@ private BoardAndAlightSlack mapTransit() { v -> tr.withRaptor(r -> r.withRelaxGeneralizedCostAtDestination(v)) ); } + setIfNotNull(req.extraSearchCoachReluctance, tr::setExtraSearchCoachReluctance); }); return new BoardAndAlightSlack( diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java index 1d985f0e555..7e0f8ea1dab 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java @@ -199,6 +199,9 @@ public abstract class RoutingResource { @QueryParam("carReluctance") protected Double carReluctance; + @QueryParam("extraSearchCoachReluctance") + protected Double extraSearchCoachReluctance; + /** * How much worse is waiting for a transit vehicle than being on a transit vehicle, as a * multiplier. The default value treats wait and on-vehicle time as the same. diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java new file mode 100644 index 00000000000..e80e106ce1e --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java @@ -0,0 +1,53 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.api.response.StopArrivals; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.multicriteria.McRaptorWorkerResult; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ConcurrentCompositeWorker implements RaptorWorker { + + private static final Logger LOG = LoggerFactory.getLogger(ConcurrentCompositeWorker.class); + + private final RaptorWorker mainWorker; + private final RaptorWorker alternativeWorker; + + ConcurrentCompositeWorker(RaptorWorker mainWorker, RaptorWorker alternativeWorker) { + this.mainWorker = mainWorker; + this.alternativeWorker = alternativeWorker; + } + + @Override + public RaptorWorkerResult route() { + if (OTPFeature.ParallelRouting.isOn()) { + var mainResultFuture = CompletableFuture.supplyAsync(mainWorker::route); + var alternativeResultFuture = CompletableFuture.supplyAsync(alternativeWorker::route); + + try { + return new RaptorWorkerResultComposite<>( + mainResultFuture.get(), + alternativeResultFuture.get() + ); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } else { + var mainResult = mainWorker.route(); + var alternativeResult = alternativeWorker.route(); + return new RaptorWorkerResultComposite<>(mainResult, alternativeResult); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java new file mode 100644 index 00000000000..9be309650aa --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java @@ -0,0 +1,141 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.function.Function; +import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.model.GenericLocation; +import org.opentripplanner.raptor.api.model.RaptorAccessEgress; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorRequest; +import org.opentripplanner.raptor.api.request.SearchParams; +import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; +import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.FactorStrategy; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.IndexBasedFactorStrategy; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; + +public class EnturHackSorlandsBanen { + + private static final double SOUTH_BOARDER_LIMIT = 59.1; + private static final int MIN_DISTANCE_LIMIT = 120_000; + + public static boolean match(RaptorRequest mcRequest) { + return mcRequest.extraSearchCoachReluctance > 0.1; + } + + public static RaptorWorker worker( + RaptorConfig config, + RaptorTransitDataProvider transitData, + RaptorRequest mcRequest, + Heuristics destinationHeuristics + ) { + //noinspection unchecked + RaptorTransitDataProvider altTransitData = (RaptorTransitDataProvider) ( + (RaptorRoutingRequestTransitData) transitData + ).enturHackSorlandsbanen(mapFactors(mcRequest.extraSearchCoachReluctance)); + + return new ConcurrentCompositeWorker<>( + config.createMcWorker(transitData, mcRequest, destinationHeuristics), + config.createMcWorker(altTransitData, mcRequest, destinationHeuristics) + ); + } + + public static RaptorRequest enableHack( + RaptorRequest raptorRequest, + RouteRequest request, + TransitLayer transitLayer + ) { + if (request.preferences().transit().extraSearchCoachReluctance() < 0.1) { + return raptorRequest; + } + + SearchParams params = raptorRequest.searchParams(); + + WgsCoordinate from = findStopCoordinate(request.from(), params.accessPaths(), transitLayer); + WgsCoordinate to = findStopCoordinate(request.to(), params.egressPaths(), transitLayer); + + if (from.latitude() > SOUTH_BOARDER_LIMIT && to.latitude() > SOUTH_BOARDER_LIMIT) { + return raptorRequest; + } + + double distanceMeters = SphericalDistanceLibrary.distance( + from.latitude(), + from.longitude(), + to.latitude(), + to.longitude() + ); + + if (distanceMeters < MIN_DISTANCE_LIMIT) { + return raptorRequest; + } + + raptorRequest.extraSearchCoachReluctance = + request.preferences().transit().extraSearchCoachReluctance(); + return raptorRequest; + } + + /* private methods */ + + private static Function mapFactors( + final double extraSearchCoachReluctance + ) { + return (FactorStrategy originalFactors) -> { + int[] modeReluctance = new int[TransitMode.values().length]; + for (TransitMode mode : TransitMode.values()) { + int index = mode.ordinal(); + int originalFactor = originalFactors.factor(index); + modeReluctance[index] = + mode == TransitMode.COACH + ? (int) (extraSearchCoachReluctance * originalFactor + 0.5) + : originalFactor; + } + return new IndexBasedFactorStrategy(modeReluctance); + }; + } + + /** + * Find a coordinate matching the given location, in order: + * - First return the coordinate of the location if it exists. + * - Then loop through the access/egress stops and try to find the + * stop or station given by the location id, return the stop/station coordinate. + * - Return the fist stop in the access/egress list coordinate. + */ + @SuppressWarnings("ConstantConditions") + private static WgsCoordinate findStopCoordinate( + GenericLocation location, + Collection accessEgress, + TransitLayer transitLayer + ) { + if (location.lat != null) { + return new WgsCoordinate(location.lat, location.lng); + } + + StopLocation firstStop = null; + for (RaptorAccessEgress it : accessEgress) { + StopLocation stop = transitLayer.getStopByIndex(it.stop()); + if (stop.getId().equals(location.stopId)) { + return stop.getCoordinate(); + } + if (idIsParentStation(stop, location.stopId)) { + return stop.getParentStation().getCoordinate(); + } + if (firstStop == null) { + firstStop = stop; + } + } + return firstStop.getCoordinate(); + } + + private static boolean idIsParentStation(StopLocation stop, FeedScopedId pId) { + return stop.getParentStation() != null && stop.getParentStation().getId().equals(pId); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java new file mode 100644 index 00000000000..94267b3e7c2 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java @@ -0,0 +1,51 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; + +final class PathKey { + + private final int hash; + + PathKey(RaptorPath path) { + this.hash = hash(path); + } + + private static int hash(RaptorPath path) { + if (path == null) { + return 0; + } + int result = 1; + + PathLeg leg = path.accessLeg(); + + while (!leg.isEgressLeg()) { + result = 31 * result + leg.toStop(); + result = 31 * result + leg.toTime(); + + if (leg.isTransitLeg()) { + result = 31 * result + leg.asTransitLeg().trip().pattern().debugInfo().hashCode(); + } + leg = leg.nextLeg(); + } + result = 31 * result + leg.toTime(); + + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o.getClass() != PathKey.class) { + return false; + } + return hash == ((PathKey) o).hash; + } + + @Override + public int hashCode() { + return hash; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java new file mode 100644 index 00000000000..094e652fc4c --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java @@ -0,0 +1,88 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RaptorWorkerResultComposite + implements RaptorWorkerResult { + + private static final Logger LOG = LoggerFactory.getLogger(RaptorWorkerResultComposite.class); + + private RaptorWorkerResult mainResult; + private RaptorWorkerResult alternativeResult; + + public RaptorWorkerResultComposite( + RaptorWorkerResult mainResult, + RaptorWorkerResult alternativeResult + ) { + this.mainResult = mainResult; + this.alternativeResult = alternativeResult; + } + + @Override + public Collection> extractPaths() { + Map> paths = new HashMap<>(); + addAll(paths, mainResult.extractPaths()); + addExtraRail(paths, alternativeResult.extractPaths()); + return paths.values(); + } + + @Override + public SingleCriteriaStopArrivals extractBestOverallArrivals() { + return mainResult.extractBestOverallArrivals(); + } + + @Override + public SingleCriteriaStopArrivals extractBestTransitArrivals() { + return mainResult.extractBestTransitArrivals(); + } + + @Override + public SingleCriteriaStopArrivals extractBestNumberOfTransfers() { + return mainResult.extractBestNumberOfTransfers(); + } + + @Override + public boolean isDestinationReached() { + return mainResult.isDestinationReached(); + } + + private void addExtraRail(Map> map, Collection> paths) { + paths.forEach(p -> { + if (hasRail(p)) { + var v = map.put(new PathKey(p), p); + LOG.debug("Ex.Rail {} : {}", (v == null ? "ADD " : "SKIP"), p); + } else { + LOG.debug("Ex. NOT Rail : {}", p); + } + }); + } + + private void addAll(Map> map, Collection> paths) { + paths.forEach(p -> { + var v = map.put(new PathKey(p), p); + LOG.debug("Normal {} : {}", (v == null ? "ADD " : "SKIP"), p); + }); + } + + private static boolean hasRail(RaptorPath path) { + return path + .legStream() + .filter(PathLeg::isTransitLeg) + .anyMatch(leg -> { + var trip = (TripScheduleWithOffset) leg.asTransitLeg().trip(); + var mode = trip.getOriginalTripPattern().getMode(); + return mode == TransitMode.RAIL; + }); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java index bf34d9928e4..c96956bc6b9 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java @@ -599,6 +599,14 @@ public static GraphQLFieldDefinition create( ) .build() ) + .argument( + GraphQLArgument + .newArgument() + .name("extraSearchCoachReluctance") + .description("FOR TESTING ONLY") + .type(Scalars.GraphQLFloat) + .build() + ) .dataFetcher(environment -> new TransmodelGraphQLPlanner().plan(environment)) .build(); } diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 4ae5004cf6b..3395c7261dd 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -32,6 +32,7 @@ public enum OTPFeature { """ ), FloatingBike(true, false, "Enable floating bike routing."), + HackSorlandsbanen(false, true, "Includ Sørlandsbanen"), GtfsGraphQlApi(true, false, "Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md)."), GtfsGraphQlApiRentalStationFuzzyMatching( false, diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java index 010bff4bc08..c53f0f90ae5 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java @@ -29,6 +29,9 @@ public class RaptorRequest { private final DebugRequest debug; private final RaptorTimers performanceTimers; + // HACK SØRLANDSBANEN + public double extraSearchCoachReluctance = 0.0; + private RaptorRequest() { searchParams = SearchParams.defaults(); profile = RaptorProfile.MULTI_CRITERIA; @@ -49,6 +52,7 @@ private RaptorRequest() { this.multiCriteria = builder.multiCriteria(); this.performanceTimers = builder.performanceTimers(); this.debug = builder.debug().build(); + this.extraSearchCoachReluctance = builder.extraSearchCoachReluctance; verify(); } diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java index ed2aab3de20..7bb8b538843 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java @@ -34,6 +34,9 @@ public class RaptorRequestBuilder { // Performance monitoring private RaptorTimers performanceTimers; + /** HACK SØRLANDSBANEN */ + public double extraSearchCoachReluctance; + // Algorithm private RaptorProfile profile; @@ -60,6 +63,9 @@ public RaptorRequestBuilder() { // Debug this.debug = new DebugRequestBuilder(defaults.debug()); + + // HACK SØRLANDSBANEN + this.extraSearchCoachReluctance = defaults.extraSearchCoachReluctance; } public SearchParamsBuilder searchParams() { diff --git a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java index fb96b7a8724..2e50dc2042d 100644 --- a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java +++ b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java @@ -11,6 +11,8 @@ import java.util.concurrent.Future; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.opentripplanner.ext.sorlandsbanen.EnturHackSorlandsBanen; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.raptor.RaptorService; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; @@ -134,7 +136,13 @@ private RaptorResponse createAndRunDynamicRRWorker(RaptorRequest request) // Create worker if (request.profile().is(MULTI_CRITERIA)) { - raptorWorker = config.createMcWorker(transitData, request, getDestinationHeuristics()); + // HACK SØRLANDSBANEN + if (OTPFeature.HackSorlandsbanen.isOn() && EnturHackSorlandsBanen.match(request)) { + raptorWorker = + EnturHackSorlandsBanen.worker(config, transitData, request, getDestinationHeuristics()); + } else { + raptorWorker = config.createMcWorker(transitData, request, getDestinationHeuristics()); + } } else { raptorWorker = config.createStdWorker(transitData, request); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index 0a9e46d2fe3..ad2434f4b70 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -127,7 +127,8 @@ private TransitRouterResult route() { serverContext.raptorConfig().isMultiThreaded(), accessEgresses.getAccesses(), accessEgresses.getEgresses(), - serverContext.meterRegistry() + serverContext.meterRegistry(), + transitLayer ); // Route transit diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java index 43faa3f0c40..c3bde3e1cc7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java @@ -1,5 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost; +import java.util.function.Function; import javax.annotation.Nullable; import org.opentripplanner.model.transfer.TransferConstraint; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; @@ -230,4 +231,19 @@ private int boardingCostConstrainedTransfer( // fallback to regular transfer return boardingCostRegularTransfer(firstBoarding, prevArrivalTime, boardStop, boardTime); } + + /*-- HACK SØRLANDSBANEN :: BEGIN --*/ + + public DefaultCostCalculator( + DefaultCostCalculator original, + Function modeReluctanceMapper + ) { + this.boardCostOnly = original.boardCostOnly; + this.boardAndTransferCost = original.boardAndTransferCost; + this.waitFactor = original.waitFactor; + this.transferCostOnly = original.transferCostOnly; + this.transitFactors = modeReluctanceMapper.apply(original.transitFactors); + this.stopBoardAlightTransferCosts = original.stopBoardAlightTransferCosts; + } + /*-- HACK SØRLANDSBANEN :: END --*/ } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java index fba2a7cfe38..8a14c27566d 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java @@ -6,7 +6,7 @@ *

* The class and methods are {@code final} to help the JIT compiler optimize the use of this class. */ -interface FactorStrategy { +public interface FactorStrategy { /** * Return the factor for the given index. */ diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java index 152f199248c..4c66b5b452e 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java @@ -6,12 +6,12 @@ * This class keep a facto for each index and the minimum factor for fast retrieval during Raptor * search. */ -final class IndexBasedFactorStrategy implements FactorStrategy { +public final class IndexBasedFactorStrategy implements FactorStrategy { private final int[] factors; private final int minFactor; - private IndexBasedFactorStrategy(int[] factors) { + public IndexBasedFactorStrategy(int[] factors) { this.factors = factors; this.minFactor = findMinimumFactor(factors); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java index 879536fdcd0..17b96790a5f 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java @@ -7,6 +7,7 @@ import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; +import org.opentripplanner.ext.sorlandsbanen.EnturHackSorlandsBanen; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.raptor.api.model.GeneralizedCostRelaxFunction; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; @@ -20,6 +21,8 @@ import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; import org.opentripplanner.raptor.rangeraptor.SystemErrDebugLogger; import org.opentripplanner.routing.algorithm.raptoradapter.router.performance.PerformanceTimersForRaptor; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; import org.opentripplanner.routing.api.request.DebugEventType; @@ -36,13 +39,16 @@ public class RaptorRequestMapper { private final boolean isMultiThreadedEnbled; private final MeterRegistry meterRegistry; + private final TransitLayer transitLayer; + private RaptorRequestMapper( RouteRequest request, boolean isMultiThreaded, Collection accessPaths, Collection egressPaths, long transitSearchTimeZeroEpocSecond, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + TransitLayer transitLayer ) { this.request = request; this.isMultiThreadedEnbled = isMultiThreaded; @@ -50,6 +56,7 @@ private RaptorRequestMapper( this.egressPaths = egressPaths; this.transitSearchTimeZeroEpocSecond = transitSearchTimeZeroEpocSecond; this.meterRegistry = meterRegistry; + this.transitLayer = transitLayer; } public static RaptorRequest mapRequest( @@ -58,7 +65,8 @@ public static RaptorRequest mapRequest( boolean isMultiThreaded, Collection accessPaths, Collection egressPaths, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + TransitLayer transitLayer ) { return new RaptorRequestMapper( request, @@ -66,7 +74,8 @@ public static RaptorRequest mapRequest( accessPaths, egressPaths, transitSearchTimeZero.toEpochSecond(), - meterRegistry + meterRegistry, + transitLayer ) .doMap(); } @@ -176,7 +185,7 @@ private RaptorRequest doMap() { ); } - return builder.build(); + return EnturHackSorlandsBanen.enableHack(builder.build(), request, transitLayer); } private List mapPassThroughPoints() { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java index 8c310206a01..d4785dc6f5b 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java @@ -4,6 +4,7 @@ import java.util.BitSet; import java.util.Iterator; import java.util.List; +import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.opentripplanner.framework.application.OTPFeature; @@ -27,6 +28,8 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.constrainedtransfer.ConstrainedBoardingSearch; import org.opentripplanner.routing.algorithm.raptoradapter.transit.constrainedtransfer.ConstrainedTransfersForPatterns; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.CostCalculatorFactory; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.DefaultCostCalculator; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.FactorStrategy; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.GeneralizedCostParametersMapper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.transit.model.network.RoutingTripPattern; @@ -253,4 +256,34 @@ private PriorityGroupConfigurator createTransitGroupPriorityConfigurator(RouteRe transitRequest.priorityGroupsGlobal() ); } + + /*-- HACK SØRLANDSBANEN :: BEGIN --*/ + + private RaptorRoutingRequestTransitData( + RaptorRoutingRequestTransitData original, + Function mapFactors + ) { + this.transitLayer = original.transitLayer; + this.transitSearchTimeZero = original.transitSearchTimeZero; + this.activeTripPatternsPerStop = original.activeTripPatternsPerStop; + this.patternIndex = original.patternIndex; + this.transferIndex = original.transferIndex; + this.transferService = original.transferService; + this.constrainedTransfers = original.constrainedTransfers; + this.validTransitDataStartTime = original.validTransitDataStartTime; + this.validTransitDataEndTime = original.validTransitDataEndTime; + this.generalizedCostCalculator = + new DefaultCostCalculator<>( + (DefaultCostCalculator) original.generalizedCostCalculator, + mapFactors + ); + this.slackProvider = original.slackProvider(); + } + + public RaptorTransitDataProvider enturHackSorlandsbanen( + Function mapFactors + ) { + return new RaptorRoutingRequestTransitData(this, mapFactors); + } + /*-- HACK SØRLANDSBANEN :: END --*/ } diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java index 78f30277e72..94a4a458915 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java @@ -31,6 +31,7 @@ public final class TransitPreferences implements Serializable { private final boolean includePlannedCancellations; private final boolean includeRealtimeCancellations; private final RaptorPreferences raptor; + private final double extraSearchCoachReluctance; private TransitPreferences() { this.boardSlack = this.alightSlack = DurationForEnum.of(TransitMode.class).build(); @@ -42,6 +43,7 @@ private TransitPreferences() { this.includePlannedCancellations = false; this.includeRealtimeCancellations = false; this.raptor = RaptorPreferences.DEFAULT; + this.extraSearchCoachReluctance = 0.0; } private TransitPreferences(Builder builder) { @@ -55,6 +57,7 @@ private TransitPreferences(Builder builder) { this.includePlannedCancellations = builder.includePlannedCancellations; this.includeRealtimeCancellations = builder.includeRealtimeCancellations; this.raptor = requireNonNull(builder.raptor); + this.extraSearchCoachReluctance = builder.extraSearchCoachReluctance; } public static Builder of() { @@ -165,6 +168,11 @@ public RaptorPreferences raptor() { return raptor; } + /** Zero means turned off. HACK SØRLANDSBANEN */ + public double extraSearchCoachReluctance() { + return extraSearchCoachReluctance; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -180,6 +188,7 @@ public boolean equals(Object o) { ignoreRealtimeUpdates == that.ignoreRealtimeUpdates && includePlannedCancellations == that.includePlannedCancellations && includeRealtimeCancellations == that.includeRealtimeCancellations && + extraSearchCoachReluctance == that.extraSearchCoachReluctance && raptor.equals(that.raptor) ); } @@ -196,6 +205,7 @@ public int hashCode() { ignoreRealtimeUpdates, includePlannedCancellations, includeRealtimeCancellations, + extraSearchCoachReluctance, raptor ); } @@ -245,6 +255,7 @@ public static class Builder { private boolean includePlannedCancellations; private boolean includeRealtimeCancellations; private RaptorPreferences raptor; + private double extraSearchCoachReluctance; public Builder(TransitPreferences original) { this.original = original; @@ -258,6 +269,7 @@ public Builder(TransitPreferences original) { this.includePlannedCancellations = original.includePlannedCancellations; this.includeRealtimeCancellations = original.includeRealtimeCancellations; this.raptor = original.raptor; + this.extraSearchCoachReluctance = original.extraSearchCoachReluctance; } public TransitPreferences original() { @@ -327,6 +339,11 @@ public Builder withRaptor(Consumer body) { return this; } + public Builder setExtraSearchCoachReluctance(double extraSearchCoachReluctance) { + this.extraSearchCoachReluctance = extraSearchCoachReluctance; + return this; + } + public Builder apply(Consumer body) { body.accept(this); return this; diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index f9ea136fbe2..df1ee7bb0eb 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -316,13 +316,14 @@ The board time is added to the time when going from the stop (offboard) to onboa } // TODO REMOVE THIS - builder.withRaptor(it -> - c - .of("relaxTransitSearchGeneralizedCostAtDestination") - .since(V2_3) - .summary("Whether non-optimal transit paths at the destination should be returned") - .description( - """ + builder + .withRaptor(it -> + c + .of("relaxTransitSearchGeneralizedCostAtDestination") + .since(V2_3) + .summary("Whether non-optimal transit paths at the destination should be returned") + .description( + """ Let c be the existing minimum pareto optimal generalized cost to beat. Then a trip with cost c' is accepted if the following is true: `c' < Math.round(c * relaxRaptorCostCriteria)`. @@ -332,10 +333,17 @@ The board time is added to the time when going from the stop (offboard) to onboa Values equals or less than zero is not allowed. Values greater than 2.0 are not supported, due to performance reasons. """ - ) - .asDoubleOptional() - .ifPresent(it::withRelaxGeneralizedCostAtDestination) - ); + ) + .asDoubleOptional() + .ifPresent(it::withRelaxGeneralizedCostAtDestination) + ) + .setExtraSearchCoachReluctance( + c + .of("extraSearchCoachReluctance") + .since(V2_1) + .summary("TODO") + .asDouble(dft.extraSearchCoachReluctance()) + ); } private static void mapBikePreferences(NodeAdapter root, BikePreferences.Builder builder) { diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index e7d68a2e780..bcb28bddb90 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -796,6 +796,8 @@ type QueryType { dateTime: DateTime, "Debug the itinerary-filter-chain. OTP will attach a system notice to itineraries instead of removing them. This is very convenient when tuning the filters." debugItineraryFilter: Boolean = false @deprecated(reason : "Use `itineraryFilter.debug` instead."), + "FOR TESTING ONLY" + extraSearchCoachReluctance: Float, "A list of filters for which trips should be included. A trip will be included if it matches with at least one filter. An empty list of filters means that all trips should be included. If a search include this parameter, \"whiteListed\", \"banned\" & \"modes.transportModes\" filters will be ignored." filters: [TripFilterInput!], "The start location" diff --git a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java index 39022da42ca..aeca5a36ebf 100644 --- a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java +++ b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java @@ -39,6 +39,9 @@ public class RaptorArchitectureTest { private static final Package RR_STANDARD = RANGE_RAPTOR.subPackage("standard"); private static final Package RR_STD_CONFIGURE = RR_STANDARD.subPackage("configure"); private static final Package RR_CONTEXT = RANGE_RAPTOR.subPackage("context"); + private static final Package EXT_SORLANDSBANAN_HACK = Package.of( + "org.opentripplanner.ext.sorlandsbanen" + ); /** * Packages used by standard-range-raptor and multi-criteria-range-raptor. @@ -200,7 +203,8 @@ void enforcePackageDependenciesInRaptorService() { RAPTOR_UTIL, CONFIGURE, RR_INTERNAL_API, - RR_TRANSIT + RR_TRANSIT, + EXT_SORLANDSBANAN_HACK ) .verify(); } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java index 47542782884..e3fbbc0df21 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java @@ -90,6 +90,7 @@ private static RaptorRequest map(RouteRequest request) { false, ACCESS, EGRESS, + null, null ); } From 54d522f32761aedd61ea016508f41c0fd06fc4a8 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Thu, 6 Jun 2024 10:30:49 +0200 Subject: [PATCH 07/20] Version 2.6.0-entur-12 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 585b93f8848..cf35bb2f0e2 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-SNAPSHOT + 2.6.0-entur-12 jar From 6b2b04aa9a9b027eb4d92cca002bd7bb9f5ddd4d Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Thu, 6 Jun 2024 12:09:36 +0200 Subject: [PATCH 08/20] Update serialization id --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cf35bb2f0e2..ea34d933b2c 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - EN-0067 + EN-0068 31.1 2.51.1 From 2ff5fcaaead5ddbf8faa31780c1d9abc5b549fc1 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Thu, 6 Jun 2024 12:23:44 +0200 Subject: [PATCH 09/20] Version 2.6.0-entur-13 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ea34d933b2c..43294b72518 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-entur-12 + 2.6.0-entur-13 jar From b5f602e83e771d5535776bd26a3864714831120e Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 7 Jun 2024 08:35:16 +0200 Subject: [PATCH 10/20] Version 2.6.0-entur-14 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cf8125c3cfb..08e8dd52490 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-entur-13 + 2.6.0-entur-14 jar From a00116e9c18dd6f3155355bab1d9bd5de970439c Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 14 Jun 2024 09:27:04 +0200 Subject: [PATCH 11/20] Fix too large response cancellation --- .../restapi/resources/PlannerResource.java | 5 ++-- .../opentripplanner/api/common/Message.java | 2 +- .../MaxFieldsInResultInstrumentation.java | 3 +-- .../transmodel/ResponseTooLargeException.java | 11 ++++++++ .../apis/transmodel/TransmodelGraph.java | 6 +++-- ...nprocessableRequestExecutionStrategy.java} | 22 ++++++++++------ .../support/ExecutionResultMapper.java | 26 ++++++++++++++----- src/main/resources/Message.properties | 2 +- src/main/resources/Message_de.properties | 2 +- src/main/resources/Message_es.properties | 2 +- src/main/resources/Message_fr.properties | 2 +- src/main/resources/Message_hu.properties | 2 +- src/main/resources/Message_nl.properties | 2 +- .../support/ExecutionResultMapperTest.java | 22 ++++++++++++++++ 14 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java rename src/main/java/org/opentripplanner/apis/transmodel/support/{AbortOnTimeoutExecutionStrategy.java => AbortOnUnprocessableRequestExecutionStrategy.java} (60%) diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java index 38c70851b76..9c58c1c719d 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java @@ -12,6 +12,7 @@ import org.opentripplanner.api.common.Message; import org.opentripplanner.api.error.PlannerError; import org.opentripplanner.apis.support.mapping.PlannerErrorMapper; +import org.opentripplanner.apis.transmodel.ResponseTooLargeException; import org.opentripplanner.ext.restapi.mapping.TripPlanMapper; import org.opentripplanner.ext.restapi.mapping.TripSearchMetadataMapper; import org.opentripplanner.ext.restapi.model.ElevationMetadata; @@ -104,8 +105,8 @@ public Response plan(@Context UriInfo uriInfo, @Context Request grizzlyRequest) LOG.error("System error - unhandled error case?", e); response.setError(new PlannerError(Message.SYSTEM_ERROR)); } - } catch (OTPRequestTimeoutException e) { - response.setError(new PlannerError(Message.PROCESSING_TIMEOUT)); + } catch (OTPRequestTimeoutException | ResponseTooLargeException e) { + response.setError(new PlannerError(Message.UNPROCESSABLE_REQUEST)); } catch (Exception e) { LOG.error("System error", e); response.setError(new PlannerError(Message.SYSTEM_ERROR)); diff --git a/src/main/java/org/opentripplanner/api/common/Message.java b/src/main/java/org/opentripplanner/api/common/Message.java index 9a07d8c33d6..3e568d57940 100644 --- a/src/main/java/org/opentripplanner/api/common/Message.java +++ b/src/main/java/org/opentripplanner/api/common/Message.java @@ -15,7 +15,7 @@ public enum Message { PLAN_OK(200), SYSTEM_ERROR(500), - PROCESSING_TIMEOUT(422), + UNPROCESSABLE_REQUEST(422), GRAPH_UNAVAILABLE(503), diff --git a/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java b/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java index 6d22f18783c..7cc356e5f6b 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java @@ -3,7 +3,6 @@ import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; import graphql.ExecutionResult; -import graphql.execution.AbortExecutionException; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -47,7 +46,7 @@ public InstrumentationContext beginFieldFetch( if (fetched % 10000 == 0) { LOG.debug("Fetched {} fields", fetched); if (fetched > maxFieldFetch) { - throw new AbortExecutionException( + throw new ResponseTooLargeException( "The number of fields in the GraphQL result exceeds the maximum allowed: " + maxFieldFetch ); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java b/src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java new file mode 100644 index 00000000000..a6b070e8fdd --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java @@ -0,0 +1,11 @@ +package org.opentripplanner.apis.transmodel; + +/** + * Exception thrown when the API response exceeds a configurable limit. + */ +public class ResponseTooLargeException extends RuntimeException { + + public ResponseTooLargeException(String message) { + super(message); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java index 1bff91638fd..d755c509989 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java @@ -16,7 +16,7 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.opentripplanner.apis.transmodel.support.AbortOnTimeoutExecutionStrategy; +import org.opentripplanner.apis.transmodel.support.AbortOnUnprocessableRequestExecutionStrategy; import org.opentripplanner.apis.transmodel.support.ExecutionResultMapper; import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation; import org.opentripplanner.framework.application.OTPFeature; @@ -50,7 +50,7 @@ Response executeGraphQL( int maxNumberOfResultFields, Iterable tracingTags ) { - try (var executionStrategy = new AbortOnTimeoutExecutionStrategy()) { + try (var executionStrategy = new AbortOnUnprocessableRequestExecutionStrategy()) { variables = ObjectUtils.ifNotNull(variables, new HashMap<>()); var instrumentation = createInstrumentation(maxNumberOfResultFields, tracingTags); var transmodelRequestContext = createRequestContext(serverContext); @@ -69,6 +69,8 @@ Response executeGraphQL( return ExecutionResultMapper.okResponse(result); } catch (OTPRequestTimeoutException te) { return ExecutionResultMapper.timeoutResponse(); + } catch (ResponseTooLargeException rtle) { + return ExecutionResultMapper.tooLargeResponse(rtle.getMessage()); } catch (CoercingParseValueException | UnknownOperationException e) { return ExecutionResultMapper.badRequestResponse(e.getMessage()); } catch (Exception systemError) { diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java b/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnUnprocessableRequestExecutionStrategy.java similarity index 60% rename from src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java rename to src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnUnprocessableRequestExecutionStrategy.java index d143d65421c..4d7b31d5ce4 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnUnprocessableRequestExecutionStrategy.java @@ -4,22 +4,28 @@ import graphql.schema.DataFetchingEnvironment; import java.io.Closeable; import java.util.concurrent.CompletableFuture; +import org.opentripplanner.apis.transmodel.ResponseTooLargeException; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.framework.logging.ProgressTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * To abort fetching data when a timeout occurs we have to rethrow the time-out-exception. + * To abort fetching data when a request is unprocessable (either because the execution times + * out or because the response is too large) we have to rethrow the exception. * This will prevent unresolved data-fetchers to be called. The exception is not handled * gracefully. */ -public class AbortOnTimeoutExecutionStrategy extends AsyncExecutionStrategy implements Closeable { +public class AbortOnUnprocessableRequestExecutionStrategy + extends AsyncExecutionStrategy + implements Closeable { - private static final Logger LOG = LoggerFactory.getLogger(AbortOnTimeoutExecutionStrategy.class); + private static final Logger LOG = LoggerFactory.getLogger( + AbortOnUnprocessableRequestExecutionStrategy.class + ); public static final int LOG_STEPS = 25_000; private final ProgressTracker timeoutProgressTracker = ProgressTracker.track( - "TIMEOUT! Abort GraphQL query", + "Unprocessable request. Abort GraphQL query", LOG_STEPS, -1 ); @@ -29,15 +35,15 @@ protected CompletableFuture handleFetchingException( DataFetchingEnvironment environment, Throwable e ) { - if (e instanceof OTPRequestTimeoutException te) { - logTimeoutProgress(); - throw te; + if (e instanceof OTPRequestTimeoutException || e instanceof ResponseTooLargeException) { + logCancellationProgress(); + throw (RuntimeException) e; } return super.handleFetchingException(environment, e); } @SuppressWarnings("Convert2MethodRef") - private void logTimeoutProgress() { + private void logCancellationProgress() { timeoutProgressTracker.startOrStep(m -> LOG.info(m)); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java index d0ccb198e16..aac9cb1bb7c 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java @@ -16,6 +16,11 @@ public class ExecutionResultMapper { private static final ErrorClassification API_PROCESSING_TIMEOUT = ErrorClassification.errorClassification( "ApiProcessingTimeout" ); + + private static final ErrorClassification RESPONSE_TOO_LARGE = ErrorClassification.errorClassification( + "ResponseTooLarge" + ); + private static final ErrorClassification BAD_REQUEST_ERROR = ErrorClassification.errorClassification( "BadRequestError" ); @@ -29,13 +34,11 @@ public static Response okResponse(ExecutionResult result) { } public static Response timeoutResponse() { - var error = GraphQLError - .newError() - .errorType(API_PROCESSING_TIMEOUT) - .message(OTPRequestTimeoutException.MESSAGE) - .build(); - var result = ExecutionResult.newExecutionResult().addError(error).build(); - return response(result, OtpHttpStatus.STATUS_UNPROCESSABLE_ENTITY); + return unprocessableResponse(API_PROCESSING_TIMEOUT, OTPRequestTimeoutException.MESSAGE); + } + + public static Response tooLargeResponse(String message) { + return unprocessableResponse(RESPONSE_TOO_LARGE, message); } public static Response badRequestResponse(String message) { @@ -56,4 +59,13 @@ public static Response response(ExecutionResult result, Response.StatusType stat .entity(GraphQLResponseSerializer.serialize(result)) .build(); } + + private static Response unprocessableResponse( + ErrorClassification errorClassification, + String message + ) { + var error = GraphQLError.newError().errorType(errorClassification).message(message).build(); + var result = ExecutionResult.newExecutionResult().addError(error).build(); + return response(result, OtpHttpStatus.STATUS_UNPROCESSABLE_ENTITY); + } } diff --git a/src/main/resources/Message.properties b/src/main/resources/Message.properties index 5c6011e3d3d..931b25c8b3b 100644 --- a/src/main/resources/Message.properties +++ b/src/main/resources/Message.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = We're sorry. The trip planner is temporarily unavailable. Please GRAPH_UNAVAILABLE = We're sorry. The trip planner is temporarily unavailable. Please try again later. OUTSIDE_BOUNDS = Trip is not possible. You might be trying to plan a trip outside the map data boundary. -PROCESSING_TIMEOUT = The trip planner is taking too long to process your request. +UNPROCESSABLE_REQUEST = The trip planner is taking too long time or too much resources to process your request. BOGUS_PARAMETER = The request has errors that the server is not willing or able to process. LOCATION_NOT_ACCESSIBLE = The location was found, but no stops could be found within the search radius. PATH_NOT_FOUND = No trip found. There may be no transit service within the maximum specified distance or at the specified time, or your start or end point might not be safely accessible. diff --git a/src/main/resources/Message_de.properties b/src/main/resources/Message_de.properties index 1b0d8a2322e..6862a993c71 100644 --- a/src/main/resources/Message_de.properties +++ b/src/main/resources/Message_de.properties @@ -2,7 +2,7 @@ PLAN_OK = Success SYSTEM_ERROR = Es tut uns leid, leider steht der Trip-Planer momentan nicht zur Verfügung. Bitte versuchen Sie es zu einem späteren Zeitpunkt nochmal. OUTSIDE_BOUNDS = Planung nicht möglich. Vielleicht versuchen sie einen Plan außerhalb der Kartengrenzen zu planen. -PROCESSING_TIMEOUT = Der Trip-Planner braucht zu lange um die Anfrage zu bearbeiten +UNPROCESSABLE_REQUEST = Der Trip-Planner braucht zu lange um die Anfrage zu bearbeiten BOGUS_PARAMETER = Die Anfrage ist fehlerhaft so dass sie der Server nicht bearbeiten möchte oder kann. PATH_NOT_FOUND = Planung nicht möglich. Ihr Start- oder Endpunkt könnte nicht erreichbar sein. Bitte stellen sie sicher, dass ihre Anfrage innerhalb der Kartendaten ist. NO_TRANSIT_TIMES = Keine Fahrzeiten verfügbar. Das Datum kann zu weit in der Vergangenheit oder zu weit in der Zukunft liegen oder es gibt keinen Verkehrsbetrieb zu dem von Ihnen gewählten Zeitpunkt. diff --git a/src/main/resources/Message_es.properties b/src/main/resources/Message_es.properties index 0c5fe7a9395..88b7c849acb 100644 --- a/src/main/resources/Message_es.properties +++ b/src/main/resources/Message_es.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = ES-We're sorry. The trip planner is temporarily unavailable. Plea GRAPH_UNAVAILABLE = ES-We're sorry. The trip planner is temporarily unavailable. Please try again later. OUTSIDE_BOUNDS = ES-los trip is not possible. You might be trying to plan a trip outside the map data boundary. -PROCESSING_TIMEOUT = ES-los trip planner is taking too long to process your request. +UNPROCESSABLE_REQUEST = ES-los trip planner is taking too long time or too much resources to process your request. BOGUS_PARAMETER = ES-los request has errors that the server is not willing or able to process. PATH_NOT_FOUND = ES-los trip is not possible. Please check that you plan is within the bound of the map. NO_TRANSIT_TIMES = ES-Non transit times available. The date may be past or too far in the future or there may not be transit service for your trip at the time you chose. diff --git a/src/main/resources/Message_fr.properties b/src/main/resources/Message_fr.properties index 6f7b73c1997..66ff82bbc59 100644 --- a/src/main/resources/Message_fr.properties +++ b/src/main/resources/Message_fr.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = Désolé: le calculateur d'itinéraires est temporairement indisp GRAPH_UNAVAILABLE = Désolé: le calculateur d'itinéraires est temporairement indisponible. Veuillez recommencer ultérieurement. OUTSIDE_BOUNDS = Impossible de calculer un itinéraire. Vous essayez de planifier un itinéraire hors des limites de la zone couverte. -PROCESSING_TIMEOUT = Le calculateur d'itinéraires prend trop de temps pour gérer votre demande. +UNPROCESSABLE_REQUEST = Le calculateur d'itinéraires prend trop de temps ou de resources pour gérer votre demande. BOGUS_PARAMETER = Le serveur ne peut pas prendre en compte la requête, elle contient des paramètres erronés. PATH_NOT_FOUND = Impossible de calculer un itinéraire. Veuillez vérifier que le trajet demandé est inclus dans la zone couverte. NO_TRANSIT_TIMES = Aucun voyage disponible. Soit la date est trop loin dans le futur, soit il n'y a pas de service ce jour. diff --git a/src/main/resources/Message_hu.properties b/src/main/resources/Message_hu.properties index eca5fad9b10..f72f2295581 100644 --- a/src/main/resources/Message_hu.properties +++ b/src/main/resources/Message_hu.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = Sajn\u00E1ljuk. Az \u00FAtvonaltervez\u0151 \u00E1tmenetileg GRAPH_UNAVAILABLE = Sajn\u00E1ljuk. Az \u00FAtvonaltervez\u0151 \u00E1tmenetileg nem el\u00E9rhet\u0151. K\u00E9rj\u00FCk, pr\u00F3b\u00E1lja \u00FAjra k\u00E9s\u0151bb. OUTSIDE_BOUNDS = Az utaz\u00E1s nem lehets\u00E9ges. Lehet, hogy a t\u00E9rk\u00E9padat-hat\u00E1ron k\u00EDv\u00FCli utat pr\u00F3b\u00E1l megtervezni. -PROCESSING_TIMEOUT = Az utaz\u00E1stervez\u0151 t\u00FAl sok\u00E1ig tart a k\u00E9r\u00E9s feldolgoz\u00E1s\u00E1hoz. +UNPROCESSABLE_REQUEST = Az utaz\u00E1stervez\u0151 t\u00FAl sok\u00E1ig tart a k\u00E9r\u00E9s feldolgoz\u00E1s\u00E1hoz. BOGUS_PARAMETER = A k\u00E9r\u00E9s olyan hib\u00E1kat tartalmaz, amelyeket a szerver nem hajland\u00F3 vagy nem k\u00E9pes feldolgozni. LOCATION_NOT_ACCESSIBLE = A helyek megtal\u00E1lhat\u00F3ak, de meg\u00E1ll\u00F3k nem tal\u00E1lhat\u00F3 a keres\u00E9si k\u00F6rzetben. PATH_NOT_FOUND = Nem tal\u00E1lhat\u00F3 utaz\u00E1s. El\u0151fordulhat, hogy a megadott maxim\u00E1lis t\u00E1vols\u00E1gon bel\u00FCl vagy a megadott id\u0151pontban nincs t\u00F6megk\u00F6zleked\u00E9si szolg\u00E1ltat\u00E1s, vagy a kezd\u0151- vagy v\u00E9gpont nem \u00E9rhet\u0151 el biztons\u00E1gosan. diff --git a/src/main/resources/Message_nl.properties b/src/main/resources/Message_nl.properties index 1e18c998264..2a7a1b7700b 100644 --- a/src/main/resources/Message_nl.properties +++ b/src/main/resources/Message_nl.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = Onze excuses. De routeplanner is momenteel niet beschikbaar. Prob GRAPH_UNAVAILABLE = Onze excuses. De routeplanner is momenteel niet beschikbaar. Probeer later opnieuw. OUTSIDE_BOUNDS = Deze reis is niet mogelijk. Mogelijk probeert u een reis te plannen buiten het beschikbare gebied -PROCESSING_TIMEOUT = De routeplanner is te lang bezig met uw verzoek. +UNPROCESSABLE_REQUEST = De routeplanner is te lang bezig met uw verzoek. BOGUS_PARAMETER = Uw verzoek bevat fouten die de server niet wil or kan verwerken. PATH_NOT_FOUND = Reis is niet mogelijk. Misschien is het startpunt of de bestemming niet veilig toegankelijk. Bijvoorbeeld een straat alleen verbonden met een snelweg. NO_TRANSIT_TIMES = Geen OV-informatie beschikbaar. De datum is mogelijk te ver in het verleden of te ver in de toekomst. Of er is geen dienst voor uw reis op het moment dat u wilt. diff --git a/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java index bce971e9f1e..b697adc9288 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java @@ -34,6 +34,21 @@ class ExecutionResultMapperTest { "}" ); + public static final String TOO_LARGE_MESSAGE = + "The number of fields in the GraphQL result exceeds the maximum allowed: 100000"; + + private static final String TOO_LARGE_RESPONSE = quoteReplace( + "{'" + + "errors':[{" + + "'message':'" + + TOO_LARGE_MESSAGE + + "'," + + "'locations':[]," + + "'extensions':{'classification':'ResponseTooLarge'}" + + "}]" + + "}" + ); + public static final String SYSTEM_ERROR_MESSAGE = "A system error!"; public static final String SYSTEM_ERROR_RESPONSE = quoteReplace( @@ -62,6 +77,13 @@ void timeoutResponse() { assertEquals(TIMEOUT_RESPONSE, response.getEntity().toString()); } + @Test + void tooLargeResponse() { + var response = ExecutionResultMapper.tooLargeResponse(TOO_LARGE_MESSAGE); + assertEquals(422, response.getStatus()); + assertEquals(TOO_LARGE_RESPONSE, response.getEntity().toString()); + } + @Test void systemErrorResponse() { var response = ExecutionResultMapper.systemErrorResponse(SYSTEM_ERROR_MESSAGE); From a501413ffce66908470a4ca90be3d30bc64fb9d7 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 14 Jun 2024 10:10:16 +0200 Subject: [PATCH 12/20] Version 2.6.0-entur-15 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 08e8dd52490..6096fe12113 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-entur-14 + 2.6.0-entur-15 jar From d78225c593d801b4d814206e1b4b67844f80f6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Erik=20St=C3=B8wer?= Date: Thu, 14 Dec 2023 16:20:23 +0100 Subject: [PATCH 13/20] CircleCI config, release scripts, and pom.xml updates. --- .circleci/config.yml | 52 +++++ .circleci/prepare_release | 235 +++++++++++++++++++++++ .circleci/release | 84 ++++++++ .circleci/update_deployment_config | 46 +++++ .gitignore | 1 + pom.xml | 12 +- src/main/java/EnturUpdatePomVersion.java | 135 +++++++++++++ 7 files changed, 559 insertions(+), 6 deletions(-) create mode 100644 .circleci/config.yml create mode 100755 .circleci/prepare_release create mode 100755 .circleci/release create mode 100755 .circleci/update_deployment_config create mode 100644 src/main/java/EnturUpdatePomVersion.java diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..8931a1bb608 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +aliases: + - &jfrog-login + name: Rename jfrog environment variable for maven setting.xml + command: | + echo "export JFROG_USER=$ARTIFACTORY_USER" >> $BASH_ENV + echo "export JFROG_PASS=$ARTIFACTORY_PASSWORD" >> $BASH_ENV + - &post-build + name: Update deployment with OTP version + command: | + sudo apt-get update + sudo apt-get install libxml2-utils + chmod u+x .circleci/update_deployment_config + .circleci/update_deployment_config + +jobs: + build: + docker: + - image: cimg/openjdk:21.0-node + environment: + DEBIAN_FRONTEND: "noninteractive" + MAVEN_OPTS: -Xmx6G + resource_class: xlarge + steps: + - checkout + - restore_cache: + keys: + - dep-cache-{{ checksum "pom.xml" }} + # fallback to the most recent cache if there is no exact match for this pom.xml + - dep-cache- + - run: wget https://raw.githubusercontent.com/entur/circleci-toolbox-image-java11/master/tools/m2/settings.xml -O .circleci/settings.xml + - run: *jfrog-login + - run: mvn org.apache.maven.plugins:maven-dependency-plugin:3.1.0:go-offline -s .circleci/settings.xml + - save_cache: + paths: + - ~/.m2 + key: dep-cache-{{ checksum "pom.xml" }} + - run: mvn install -PprettierSkip org.apache.maven.plugins:maven-deploy-plugin:2.8.2:deploy -s .circleci/settings.xml -DaltDeploymentRepository=snapshots::default::https://entur2.jfrog.io/entur2/libs-release-local + - run: *post-build + +workflows: + version: 2 + release: + jobs: + - build: + name: build-release + context: global + filters: + branches: + only: + - otp2_entur_develop \ No newline at end of file diff --git a/.circleci/prepare_release b/.circleci/prepare_release new file mode 100755 index 00000000000..5b2ad0e23c4 --- /dev/null +++ b/.circleci/prepare_release @@ -0,0 +1,235 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ENTUR_DEVELOP=otp2_entur_develop +REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +STATUS_FILE=".prepare_release.tmp" +STATUS="" +DRY_RUN="" +OTP_BASE="" + +function main() { + setup "$@" + resumePreviousExecution + resetEnturDevelop + rebaseAndMergeExtBranch otp2_ext_config + rebaseAndMergeExtBranch otp2_ext_hack_sorlandsbanen + logSuccess +} + +function setup() { + if [[ $# -eq 2 && "$1" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + OTP_BASE="$2" + elif [[ $# -eq 1 ]] ; then + OTP_BASE="$1" + else + printHelp + exit 1 + fi + + echo "" + echo "Options: ${DRY_RUN}" + echo "Git base branch/commit: ${OTP_BASE}" + echo "Entur develop branch: ${ENTUR_DEVELOP}" + echo "Entur remote repo(pull/push): ${REMOTE_REPO}" + echo "" + + if git diff-index --quiet HEAD --; then + echo "" + echo "OK - No local changes, prepare to checkout '${ENTUR_DEVELOP}'" + echo "" + else + echo "" + echo "You have local modification, the script will abort. Nothing done!" + exit 2 + fi + + git fetch ${REMOTE_REPO} +} + +# This script create a status file '.prepare_release.tmp'. This file is used to resume the +# script in the same spot as where is left when the error occurred. This allow us to fix the +# problem (merge conflict or compile error) and re-run the script to complete the proses. +function resumePreviousExecution() { + readStatus + + if [[ -n "${STATUS}" ]] ; then + echo "" + echo "Resume: ${STATUS}?" + echo "" + echo " If all problems are resolved you may continue." + echo " Exit to clear status and start over." + echo "" + + ANSWER="" + while [[ ! "$ANSWER" =~ [yx] ]]; do + echo "Do you want to resume: [y:Yes, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function resetEnturDevelop() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## RESET '${ENTUR_DEVELOP}' TO '${OTP_BASE}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "Would you like to reset the '${ENTUR_DEVELOP}' to '${OTP_BASE}'? " + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${ENTUR_DEVELOP}'" + git checkout ${ENTUR_DEVELOP} + + echo "" + echo "Reset '${ENTUR_DEVELOP}' branch to '${OTP_BASE}' (hard)" + git reset --hard "${OTP_BASE}" + echo "" + fi +} + +function rebaseAndMergeExtBranch() { + EXT_BRANCH="$1" + EXT_STATUS_REBASE="Rebase '${EXT_BRANCH}'" + EXT_STATUS_COMPILE="Compile '${EXT_BRANCH}'" + + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## REBASE AND MERGE '${EXT_BRANCH}' INTO '${ENTUR_DEVELOP}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "You are about to rebase and merge '${EXT_BRANCH}' into '${ENTUR_DEVELOP}'. Any local" + echo "modification in the '${EXT_BRANCH}' will be lost." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${EXT_BRANCH}'" + git checkout "${EXT_BRANCH}" + + echo "" + echo "Reset to '${REMOTE_REPO}/${EXT_BRANCH}'" + git reset --hard "${REMOTE_REPO}/${EXT_BRANCH}" + + echo "" + echo "Top 2 commits in '${EXT_BRANCH}'" + echo "-------------------------------------------------------------------------------------------" + git --no-pager log -2 + echo "-------------------------------------------------------------------------------------------" + echo "" + echo "You are about to rebase the TOP COMMIT ONLY(see above). Check that the " + echo "'${EXT_BRANCH}' only have ONE commit that you want to keep." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Rebase '${EXT_BRANCH}' onto '${ENTUR_DEVELOP}'" + setStatus "${EXT_STATUS_REBASE}" + git rebase --onto ${ENTUR_DEVELOP} HEAD~1 + fi + fi + + if [[ "${STATUS}" == "${EXT_STATUS_REBASE}" || "${STATUS}" == "${EXT_STATUS_COMPILE}" ]] ; then + # Reset status in case the test-compile fails. We need to do this because the status file + # is deleted after reading the status in the setup() function. + setStatus "${EXT_STATUS_COMPILE}" + + mvn clean test-compile + clearStatus + + echo "" + echo "Push '${EXT_BRANCH}'" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f + else + echo "Skip: git push -f (--dryRun)" + fi + + echo "" + echo "Checkout '${ENTUR_DEVELOP}' and merge in '${EXT_BRANCH}'" + git checkout "${ENTUR_DEVELOP}" + git merge "${EXT_BRANCH}" + fi +} + +function logSuccess() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## PREPARE RELEASE DONE -- SUCCESS" + echo "## ------------------------------------------------------------------------------------- ##" + echo " - '${REMOTE_REPO}/${ENTUR_DEVELOP}' reset to '${OTP_BASE}'" + echo " - 'otp2_ext_config' merged" + echo " - 'otp2_ext_hack_sorlandsbanen' merged" + echo "" + echo "" +} + +function whatDoYouWant() { + echo "" + ANSWER="" + + if [[ -n "${STATUS}" ]] ; then + # Skip until process is resumed + ANSWER="s" + else + while [[ ! "$ANSWER" =~ [ysx] ]]; do + echo "Do you want to continue: [y:Yes, s:Skip, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function setStatus() { + STATUS="$1" + echo "$STATUS" > "${STATUS_FILE}" +} + +function readStatus() { + if [[ -f "${STATUS_FILE}" ]] ; then + STATUS=`cat $STATUS_FILE` + rm "$STATUS_FILE" + else + STATUS="" + fi +} + +function clearStatus() { + STATUS="" + rm "${STATUS_FILE}" +} + +function printHelp() { + echo "" + echo "This script take ONE argument , the base **branch** or **commit** to use for the" + echo "release. The '${ENTUR_DEVELOP}' branch is reset to this commit and then the extension" + echo "branches is rebased onto that. The 'release' script is used to complete the release." + echo "It tag and push all changes to remote git repo." + echo "" + echo "Options:" + echo " --dryRun : Run script locally, nothing is pushed to remote server." + echo "" + echo "Usage:" + echo " $ .circleci/prepare_release otp/dev-2.x" + echo " $ .circleci/prepare_release --dryRun otp/dev-2.x" + echo "" +} + +main "$@" diff --git a/.circleci/release b/.circleci/release new file mode 100755 index 00000000000..9cac0d97958 --- /dev/null +++ b/.circleci/release @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +GIT_REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +MASTER_BRANCH=otp2_entur_develop +TAGS_FILE=target/git-entur-tags.txt +DRY_RUN="" + +function main() { + setup "$@" + listAllTags + mergeInOldReleaseWithNoChanges + setPomVersion + tagRelease + pushToRemote +} + +function setup() { + echo "" + echo "git fetch ${GIT_REMOTE_REPO}" + git fetch ${GIT_REMOTE_REPO} + + echo "Verify current branch is ${MASTER_BRANCH} " + git status | grep -q "On branch ${MASTER_BRANCH}" + + if [[ "${1+x}" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + fi +} + +function listAllTags() { + ## List all Entur tags to allow the UpdatePomVersion java program find the next version number + echo "" + echo "Dump all entur tags to ${TAGS_FILE}" + git tag -l | grep entur > ${TAGS_FILE} +} + +function setPomVersion() { + echo "" + echo "Update pom.xml with new version" + javac -d target/classes src/main/java/EnturUpdatePomVersion.java + + VERSION="`java -cp target/classes EnturUpdatePomVersion ${TAGS_FILE}`" + echo "" + echo "New version set: ${VERSION}" + echo "" + + ## Verify everything builds and tests run + echo "" + mvn clean test + + ## Add [ci skip] here before moving this to the CI server + echo "" + echo "Add and commit pom.xml" + git commit -m "Version ${VERSION}" pom.xml +} + +function mergeInOldReleaseWithNoChanges() { + echo "" + echo "Merge the old version of '${GIT_REMOTE_REPO}' into the new version. This only keep " + echo "a reference to the old version, the resulting tree of the merge is that of the new" + echo "branch head, effectively ignoring all changes from the old release." + git merge -s ours "${GIT_REMOTE_REPO}/${MASTER_BRANCH}" -m "Merge old release into '${MASTER_BRANCH}' - NO CHANGES COPIED OVER" +} + + +function tagRelease() { + echo "" + echo "Tag version ${VERSION}" + git tag -a v${VERSION} -m "Version ${VERSION}" +} + +function pushToRemote() { + echo "" + echo "Push pom.xml and new tag" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} + else + echo "Skip: push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} (--dryRun)" + fi +} + +main "$@" diff --git a/.circleci/update_deployment_config b/.circleci/update_deployment_config new file mode 100755 index 00000000000..64550f19797 --- /dev/null +++ b/.circleci/update_deployment_config @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## +## This script run AFTER OTP is build on the CI Server. It checkout the +## deployment config and update the version number, commit and push. This +## trigger the deployment config build witch create and deploy a new OTP2 +## docker image. +## +## Note! There is no need to run this script locally, unless you want to debug it. +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## + + +set -euo pipefail + +OTP_DEPLOYMENT_CONFIG=target/otp-deployment-config +DOCKER_FILE=Dockerfile + +VERSION=$(xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml) + +echo "Checkout otp-deplyment-config in ${OTP_DEPLOYMENT_CONFIG}" +git clone -n https://github.com/entur/otp-deployment-config.git --depth 1 ${OTP_DEPLOYMENT_CONFIG} + +pushd ${OTP_DEPLOYMENT_CONFIG} + +echo "Checkout latest version of master Dockerfile" +git checkout master ${DOCKER_FILE} + +echo "Update OTP version number in ${DOCKER_FILE}" +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -E -i '' "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +else + sed -E -i "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +fi + +if [[ "$CIRCLE_USERNAME" != "" ]]; then + git config user.email "circleci@entur.no" + git config user.name "circleci ($CIRCLE_USERNAME)" +fi + +echo "Add and commit Dockerfile" +git commit -m "New OTP2 Version ${VERSION}" ${DOCKER_FILE} + +echo "Push otp-deployment-config to GitHub" +git push + +popd \ No newline at end of file diff --git a/.gitignore b/.gitignore index a90f06cdcb0..3bc96654225 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .project .pydevproject .settings +.prepare_release.tmp .sonar .nvmrc *~ diff --git a/pom.xml b/pom.xml index 6f202b592f8..11818765947 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 150 + EN-0068 31.1 2.51.1 @@ -84,11 +84,11 @@ - - github - OpenTripPlanner Maven Repository on Github Packages - https://maven.pkg.github.com/${GITHUB_REPOSITORY}/ - + + snapshots + entur2-snapshots + https://entur2.jfrog.io/entur2/libs-snapshot-local + diff --git a/src/main/java/EnturUpdatePomVersion.java b/src/main/java/EnturUpdatePomVersion.java new file mode 100644 index 00000000000..97bd72b3c6e --- /dev/null +++ b/src/main/java/EnturUpdatePomVersion.java @@ -0,0 +1,135 @@ +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +public class EnturUpdatePomVersion { + + private static final String VERSION_SEP = "-"; + private static final String ENTUR_PREFIX = "entur" + VERSION_SEP; + private static final Path POM_FILE = Path.of("pom.xml"); + + private final List tags = new ArrayList<>(); + private final List pomFile = new ArrayList<>(); + private String mainVersion; + private int versionNumber = 0; + + public static void main(String[] args) { + try { + new EnturUpdatePomVersion().withArgs(args).run(); + } catch (Exception e) { + System.err.println(e.getMessage()); + e.printStackTrace(System.err); + System.exit(10); + } + } + + private EnturUpdatePomVersion withArgs(String[] args) throws IOException { + if (args.length != 1 || args[0].matches("(?i)-h|--help")) { + printHelp(); + System.exit(1); + } + String arg = args[0]; + + if (arg.matches("\\d+")) { + versionNumber = resolveVersionFromNumericString(arg); + } else { + tags.addAll(readTagsFromFile(arg)); + } + return this; + } + + private void run() throws IOException { + readAndReplaceVersion(); + replacePomFile(); + } + + public void readAndReplaceVersion() throws IOException { + var pattern = Pattern.compile( + "(\\s*)(\\d+.\\d+.\\d+)" + + VERSION_SEP + + "(" + + ENTUR_PREFIX + + "\\d+|SNAPSHOT)(\\s*)" + ); + boolean found = false; + int i = 0; + + for (String line : Files.readAllLines(POM_FILE, UTF_8)) { + // Look for the version in the 25 first lines + if (!found) { + var m = pattern.matcher(line); + if (m.matches()) { + mainVersion = m.group(2); + String newVersion = mainVersion + VERSION_SEP + ENTUR_PREFIX + resolveVersionNumber(); + line = m.group(1) + newVersion + m.group(4); + System.out.println(newVersion); + found = true; + } + if (++i == 25) { + throw new IllegalStateException( + "Version not found in first 25 lines of the pom.xml file." + ); + } + } + pomFile.add(line); + } + if (!found) { + throw new IllegalStateException( + "Version not found in 'pom.xml'. Nothing matching pattern: " + pattern + ); + } + } + + public void replacePomFile() throws IOException { + Files.delete(POM_FILE); + Files.write(POM_FILE, pomFile, UTF_8); + } + + private static void printHelp() { + System.err.println( + "Use this small program to replace the OTP version '2.1.0-SNAPSHOT' \n" + + "with a new version number with a Entur qualifier like '2.1.0-entur-1'.\n" + + "\n" + + "Usage:\n" + + " $ java -cp .circleci UpdatePomVersion \n" + ); + } + + private int resolveVersionNumber() { + var pattern = Pattern.compile("v" + mainVersion + VERSION_SEP + ENTUR_PREFIX + "(\\d+)"); + int maxTagVersion = tags + .stream() + .mapToInt(tag -> { + var m = pattern.matcher(tag); + return m.matches() ? Integer.parseInt(m.group(1)) : -999; + }) + .max() + .orElse(-999); + + return 1 + Math.max(maxTagVersion, versionNumber); + } + + public static int resolveVersionFromNumericString(String arg) { + try { + return Integer.parseInt(arg); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Unable to parse input, decimal number expected: '" + arg + "'" + ); + } + } + + private static Collection readTagsFromFile(String arg) throws IOException { + var tags = Files.readAllLines(Path.of(arg)); + if (tags.isEmpty()) { + throw new IllegalStateException("Unable to load git tags from file: " + arg); + } + return tags; + } +} From ae3b16e77bb6cc36bc6c26e51320ce9cc4fd1e0d Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 19 Dec 2023 21:26:49 +0100 Subject: [PATCH 14/20] =?UTF-8?q?hack:=20Add=20train=20option=20for=20S?= =?UTF-8?q?=C3=B8rlandsbanen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Configuration.md | 1 + docs/RouteRequest.md | 1 + .../resources/RequestToPreferencesMapper.java | 1 + .../restapi/resources/RoutingResource.java | 3 + .../ConcurrentCompositeWorker.java | 53 +++++++ .../sorlandsbanen/EnturHackSorlandsBanen.java | 141 ++++++++++++++++++ .../ext/sorlandsbanen/PathKey.java | 51 +++++++ .../RaptorWorkerResultComposite.java | 88 +++++++++++ .../apis/transmodel/model/plan/TripQuery.java | 8 + .../framework/application/OTPFeature.java | 1 + .../raptor/api/request/RaptorRequest.java | 4 + .../api/request/RaptorRequestBuilder.java | 6 + .../service/RangeRaptorDynamicSearch.java | 10 +- .../raptoradapter/router/TransitRouter.java | 3 +- .../transit/cost/DefaultCostCalculator.java | 16 ++ .../transit/cost/FactorStrategy.java | 2 +- .../cost/IndexBasedFactorStrategy.java | 4 +- .../transit/mappers/RaptorRequestMapper.java | 17 ++- .../RaptorRoutingRequestTransitData.java | 33 ++++ .../preference/TransitPreferences.java | 17 +++ .../routerequest/RouteRequestConfig.java | 30 ++-- .../apis/transmodel/schema.graphql | 2 + .../raptor/RaptorArchitectureTest.java | 6 +- .../mappers/RaptorRequestMapperTest.java | 1 + 24 files changed, 478 insertions(+), 21 deletions(-) create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java create mode 100644 src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java diff --git a/docs/Configuration.md b/docs/Configuration.md index 05611e23628..2f34b598fa7 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -227,6 +227,7 @@ Here is a list of all features which can be toggled on/off and their default val | `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | | `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature if](sandbox/MapboxVectorTilesApi.md) you want a stable map tiles API. | ✓️ | | | `FloatingBike` | Enable floating bike routing. | ✓️ | | +| `HackSorlandsbanen` | Includ Sørlandsbanen | | ✓️ | | `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | | `GtfsGraphQlApiRentalStationFuzzyMatching` | Does vehicleRentalStation query also allow ids that are not feed scoped. | | | | `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | diff --git a/docs/RouteRequest.md b/docs/RouteRequest.md index 0fd9ec47f89..790984ac02c 100644 --- a/docs/RouteRequest.md +++ b/docs/RouteRequest.md @@ -23,6 +23,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe | elevatorBoardTime | `integer` | How long does it take to get on an elevator, on average. | *Optional* | `90` | 2.0 | | elevatorHopCost | `integer` | What is the cost of travelling one floor on an elevator? | *Optional* | `20` | 2.0 | | elevatorHopTime | `integer` | How long does it take to advance one floor on an elevator? | *Optional* | `20` | 2.0 | +| extraSearchCoachReluctance | `double` | TODO | *Optional* | `0.0` | 2.1 | | geoidElevation | `boolean` | If true, the Graph's ellipsoidToGeoidDifference is applied to all elevations returned by this query. | *Optional* | `false` | 2.0 | | ignoreRealtimeUpdates | `boolean` | When true, real-time updates are ignored during this search. | *Optional* | `false` | 2.0 | | [intersectionTraversalModel](#rd_intersectionTraversalModel) | `enum` | The model that computes the costs of turns. | *Optional* | `"simple"` | 2.2 | diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java index 6248f71e214..f178af9abc0 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java @@ -132,6 +132,7 @@ private BoardAndAlightSlack mapTransit() { v -> tr.withRaptor(r -> r.withRelaxGeneralizedCostAtDestination(v)) ); } + setIfNotNull(req.extraSearchCoachReluctance, tr::setExtraSearchCoachReluctance); }); return new BoardAndAlightSlack( diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java index 3ae029fdb2d..b4912aacb1d 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java @@ -203,6 +203,9 @@ public abstract class RoutingResource { @QueryParam("carReluctance") protected Double carReluctance; + @QueryParam("extraSearchCoachReluctance") + protected Double extraSearchCoachReluctance; + /** * How much worse is waiting for a transit vehicle than being on a transit vehicle, as a * multiplier. The default value treats wait and on-vehicle time as the same. diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java new file mode 100644 index 00000000000..e80e106ce1e --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/ConcurrentCompositeWorker.java @@ -0,0 +1,53 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.api.response.StopArrivals; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.multicriteria.McRaptorWorkerResult; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ConcurrentCompositeWorker implements RaptorWorker { + + private static final Logger LOG = LoggerFactory.getLogger(ConcurrentCompositeWorker.class); + + private final RaptorWorker mainWorker; + private final RaptorWorker alternativeWorker; + + ConcurrentCompositeWorker(RaptorWorker mainWorker, RaptorWorker alternativeWorker) { + this.mainWorker = mainWorker; + this.alternativeWorker = alternativeWorker; + } + + @Override + public RaptorWorkerResult route() { + if (OTPFeature.ParallelRouting.isOn()) { + var mainResultFuture = CompletableFuture.supplyAsync(mainWorker::route); + var alternativeResultFuture = CompletableFuture.supplyAsync(alternativeWorker::route); + + try { + return new RaptorWorkerResultComposite<>( + mainResultFuture.get(), + alternativeResultFuture.get() + ); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } else { + var mainResult = mainWorker.route(); + var alternativeResult = alternativeWorker.route(); + return new RaptorWorkerResultComposite<>(mainResult, alternativeResult); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java new file mode 100644 index 00000000000..9be309650aa --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturHackSorlandsBanen.java @@ -0,0 +1,141 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.function.Function; +import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.model.GenericLocation; +import org.opentripplanner.raptor.api.model.RaptorAccessEgress; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorRequest; +import org.opentripplanner.raptor.api.request.SearchParams; +import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; +import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.FactorStrategy; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.IndexBasedFactorStrategy; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; + +public class EnturHackSorlandsBanen { + + private static final double SOUTH_BOARDER_LIMIT = 59.1; + private static final int MIN_DISTANCE_LIMIT = 120_000; + + public static boolean match(RaptorRequest mcRequest) { + return mcRequest.extraSearchCoachReluctance > 0.1; + } + + public static RaptorWorker worker( + RaptorConfig config, + RaptorTransitDataProvider transitData, + RaptorRequest mcRequest, + Heuristics destinationHeuristics + ) { + //noinspection unchecked + RaptorTransitDataProvider altTransitData = (RaptorTransitDataProvider) ( + (RaptorRoutingRequestTransitData) transitData + ).enturHackSorlandsbanen(mapFactors(mcRequest.extraSearchCoachReluctance)); + + return new ConcurrentCompositeWorker<>( + config.createMcWorker(transitData, mcRequest, destinationHeuristics), + config.createMcWorker(altTransitData, mcRequest, destinationHeuristics) + ); + } + + public static RaptorRequest enableHack( + RaptorRequest raptorRequest, + RouteRequest request, + TransitLayer transitLayer + ) { + if (request.preferences().transit().extraSearchCoachReluctance() < 0.1) { + return raptorRequest; + } + + SearchParams params = raptorRequest.searchParams(); + + WgsCoordinate from = findStopCoordinate(request.from(), params.accessPaths(), transitLayer); + WgsCoordinate to = findStopCoordinate(request.to(), params.egressPaths(), transitLayer); + + if (from.latitude() > SOUTH_BOARDER_LIMIT && to.latitude() > SOUTH_BOARDER_LIMIT) { + return raptorRequest; + } + + double distanceMeters = SphericalDistanceLibrary.distance( + from.latitude(), + from.longitude(), + to.latitude(), + to.longitude() + ); + + if (distanceMeters < MIN_DISTANCE_LIMIT) { + return raptorRequest; + } + + raptorRequest.extraSearchCoachReluctance = + request.preferences().transit().extraSearchCoachReluctance(); + return raptorRequest; + } + + /* private methods */ + + private static Function mapFactors( + final double extraSearchCoachReluctance + ) { + return (FactorStrategy originalFactors) -> { + int[] modeReluctance = new int[TransitMode.values().length]; + for (TransitMode mode : TransitMode.values()) { + int index = mode.ordinal(); + int originalFactor = originalFactors.factor(index); + modeReluctance[index] = + mode == TransitMode.COACH + ? (int) (extraSearchCoachReluctance * originalFactor + 0.5) + : originalFactor; + } + return new IndexBasedFactorStrategy(modeReluctance); + }; + } + + /** + * Find a coordinate matching the given location, in order: + * - First return the coordinate of the location if it exists. + * - Then loop through the access/egress stops and try to find the + * stop or station given by the location id, return the stop/station coordinate. + * - Return the fist stop in the access/egress list coordinate. + */ + @SuppressWarnings("ConstantConditions") + private static WgsCoordinate findStopCoordinate( + GenericLocation location, + Collection accessEgress, + TransitLayer transitLayer + ) { + if (location.lat != null) { + return new WgsCoordinate(location.lat, location.lng); + } + + StopLocation firstStop = null; + for (RaptorAccessEgress it : accessEgress) { + StopLocation stop = transitLayer.getStopByIndex(it.stop()); + if (stop.getId().equals(location.stopId)) { + return stop.getCoordinate(); + } + if (idIsParentStation(stop, location.stopId)) { + return stop.getParentStation().getCoordinate(); + } + if (firstStop == null) { + firstStop = stop; + } + } + return firstStop.getCoordinate(); + } + + private static boolean idIsParentStation(StopLocation stop, FeedScopedId pId) { + return stop.getParentStation() != null && stop.getParentStation().getId().equals(pId); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java new file mode 100644 index 00000000000..94267b3e7c2 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java @@ -0,0 +1,51 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; + +final class PathKey { + + private final int hash; + + PathKey(RaptorPath path) { + this.hash = hash(path); + } + + private static int hash(RaptorPath path) { + if (path == null) { + return 0; + } + int result = 1; + + PathLeg leg = path.accessLeg(); + + while (!leg.isEgressLeg()) { + result = 31 * result + leg.toStop(); + result = 31 * result + leg.toTime(); + + if (leg.isTransitLeg()) { + result = 31 * result + leg.asTransitLeg().trip().pattern().debugInfo().hashCode(); + } + leg = leg.nextLeg(); + } + result = 31 * result + leg.toTime(); + + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o.getClass() != PathKey.class) { + return false; + } + return hash == ((PathKey) o).hash; + } + + @Override + public int hashCode() { + return hash; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java new file mode 100644 index 00000000000..094e652fc4c --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/sorlandsbanen/RaptorWorkerResultComposite.java @@ -0,0 +1,88 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RaptorWorkerResultComposite + implements RaptorWorkerResult { + + private static final Logger LOG = LoggerFactory.getLogger(RaptorWorkerResultComposite.class); + + private RaptorWorkerResult mainResult; + private RaptorWorkerResult alternativeResult; + + public RaptorWorkerResultComposite( + RaptorWorkerResult mainResult, + RaptorWorkerResult alternativeResult + ) { + this.mainResult = mainResult; + this.alternativeResult = alternativeResult; + } + + @Override + public Collection> extractPaths() { + Map> paths = new HashMap<>(); + addAll(paths, mainResult.extractPaths()); + addExtraRail(paths, alternativeResult.extractPaths()); + return paths.values(); + } + + @Override + public SingleCriteriaStopArrivals extractBestOverallArrivals() { + return mainResult.extractBestOverallArrivals(); + } + + @Override + public SingleCriteriaStopArrivals extractBestTransitArrivals() { + return mainResult.extractBestTransitArrivals(); + } + + @Override + public SingleCriteriaStopArrivals extractBestNumberOfTransfers() { + return mainResult.extractBestNumberOfTransfers(); + } + + @Override + public boolean isDestinationReached() { + return mainResult.isDestinationReached(); + } + + private void addExtraRail(Map> map, Collection> paths) { + paths.forEach(p -> { + if (hasRail(p)) { + var v = map.put(new PathKey(p), p); + LOG.debug("Ex.Rail {} : {}", (v == null ? "ADD " : "SKIP"), p); + } else { + LOG.debug("Ex. NOT Rail : {}", p); + } + }); + } + + private void addAll(Map> map, Collection> paths) { + paths.forEach(p -> { + var v = map.put(new PathKey(p), p); + LOG.debug("Normal {} : {}", (v == null ? "ADD " : "SKIP"), p); + }); + } + + private static boolean hasRail(RaptorPath path) { + return path + .legStream() + .filter(PathLeg::isTransitLeg) + .anyMatch(leg -> { + var trip = (TripScheduleWithOffset) leg.asTransitLeg().trip(); + var mode = trip.getOriginalTripPattern().getMode(); + return mode == TransitMode.RAIL; + }); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java index 5ed3264a4fd..26676d96c7b 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java @@ -616,6 +616,14 @@ Normally this is when the search is performed (now), plus a small grace period t ) .build() ) + .argument( + GraphQLArgument + .newArgument() + .name("extraSearchCoachReluctance") + .description("FOR TESTING ONLY") + .type(Scalars.GraphQLFloat) + .build() + ) .dataFetcher(environment -> new TransmodelGraphQLPlanner().plan(environment)) .build(); } diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 29489de19f2..d7bd15922fc 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -32,6 +32,7 @@ public enum OTPFeature { """ ), FloatingBike(true, false, "Enable floating bike routing."), + HackSorlandsbanen(false, true, "Includ Sørlandsbanen"), GtfsGraphQlApi(true, false, "Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md)."), GtfsGraphQlApiRentalStationFuzzyMatching( false, diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java index 010bff4bc08..c53f0f90ae5 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequest.java @@ -29,6 +29,9 @@ public class RaptorRequest { private final DebugRequest debug; private final RaptorTimers performanceTimers; + // HACK SØRLANDSBANEN + public double extraSearchCoachReluctance = 0.0; + private RaptorRequest() { searchParams = SearchParams.defaults(); profile = RaptorProfile.MULTI_CRITERIA; @@ -49,6 +52,7 @@ private RaptorRequest() { this.multiCriteria = builder.multiCriteria(); this.performanceTimers = builder.performanceTimers(); this.debug = builder.debug().build(); + this.extraSearchCoachReluctance = builder.extraSearchCoachReluctance; verify(); } diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java index ed2aab3de20..7bb8b538843 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorRequestBuilder.java @@ -34,6 +34,9 @@ public class RaptorRequestBuilder { // Performance monitoring private RaptorTimers performanceTimers; + /** HACK SØRLANDSBANEN */ + public double extraSearchCoachReluctance; + // Algorithm private RaptorProfile profile; @@ -60,6 +63,9 @@ public RaptorRequestBuilder() { // Debug this.debug = new DebugRequestBuilder(defaults.debug()); + + // HACK SØRLANDSBANEN + this.extraSearchCoachReluctance = defaults.extraSearchCoachReluctance; } public SearchParamsBuilder searchParams() { diff --git a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java index fb96b7a8724..2e50dc2042d 100644 --- a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java +++ b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java @@ -11,6 +11,8 @@ import java.util.concurrent.Future; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.opentripplanner.ext.sorlandsbanen.EnturHackSorlandsBanen; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.raptor.RaptorService; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; @@ -134,7 +136,13 @@ private RaptorResponse createAndRunDynamicRRWorker(RaptorRequest request) // Create worker if (request.profile().is(MULTI_CRITERIA)) { - raptorWorker = config.createMcWorker(transitData, request, getDestinationHeuristics()); + // HACK SØRLANDSBANEN + if (OTPFeature.HackSorlandsbanen.isOn() && EnturHackSorlandsBanen.match(request)) { + raptorWorker = + EnturHackSorlandsBanen.worker(config, transitData, request, getDestinationHeuristics()); + } else { + raptorWorker = config.createMcWorker(transitData, request, getDestinationHeuristics()); + } } else { raptorWorker = config.createStdWorker(transitData, request); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index 6a1404c3039..eaab9741570 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -127,7 +127,8 @@ private TransitRouterResult route() { serverContext.raptorConfig().isMultiThreaded(), accessEgresses.getAccesses(), accessEgresses.getEgresses(), - serverContext.meterRegistry() + serverContext.meterRegistry(), + transitLayer ); // Route transit diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java index 43faa3f0c40..c3bde3e1cc7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java @@ -1,5 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost; +import java.util.function.Function; import javax.annotation.Nullable; import org.opentripplanner.model.transfer.TransferConstraint; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; @@ -230,4 +231,19 @@ private int boardingCostConstrainedTransfer( // fallback to regular transfer return boardingCostRegularTransfer(firstBoarding, prevArrivalTime, boardStop, boardTime); } + + /*-- HACK SØRLANDSBANEN :: BEGIN --*/ + + public DefaultCostCalculator( + DefaultCostCalculator original, + Function modeReluctanceMapper + ) { + this.boardCostOnly = original.boardCostOnly; + this.boardAndTransferCost = original.boardAndTransferCost; + this.waitFactor = original.waitFactor; + this.transferCostOnly = original.transferCostOnly; + this.transitFactors = modeReluctanceMapper.apply(original.transitFactors); + this.stopBoardAlightTransferCosts = original.stopBoardAlightTransferCosts; + } + /*-- HACK SØRLANDSBANEN :: END --*/ } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java index fba2a7cfe38..8a14c27566d 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/FactorStrategy.java @@ -6,7 +6,7 @@ *

* The class and methods are {@code final} to help the JIT compiler optimize the use of this class. */ -interface FactorStrategy { +public interface FactorStrategy { /** * Return the factor for the given index. */ diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java index 152f199248c..4c66b5b452e 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/IndexBasedFactorStrategy.java @@ -6,12 +6,12 @@ * This class keep a facto for each index and the minimum factor for fast retrieval during Raptor * search. */ -final class IndexBasedFactorStrategy implements FactorStrategy { +public final class IndexBasedFactorStrategy implements FactorStrategy { private final int[] factors; private final int minFactor; - private IndexBasedFactorStrategy(int[] factors) { + public IndexBasedFactorStrategy(int[] factors) { this.factors = factors; this.minFactor = findMinimumFactor(factors); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java index 879536fdcd0..17b96790a5f 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java @@ -7,6 +7,7 @@ import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; +import org.opentripplanner.ext.sorlandsbanen.EnturHackSorlandsBanen; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.raptor.api.model.GeneralizedCostRelaxFunction; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; @@ -20,6 +21,8 @@ import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; import org.opentripplanner.raptor.rangeraptor.SystemErrDebugLogger; import org.opentripplanner.routing.algorithm.raptoradapter.router.performance.PerformanceTimersForRaptor; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; import org.opentripplanner.routing.api.request.DebugEventType; @@ -36,13 +39,16 @@ public class RaptorRequestMapper { private final boolean isMultiThreadedEnbled; private final MeterRegistry meterRegistry; + private final TransitLayer transitLayer; + private RaptorRequestMapper( RouteRequest request, boolean isMultiThreaded, Collection accessPaths, Collection egressPaths, long transitSearchTimeZeroEpocSecond, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + TransitLayer transitLayer ) { this.request = request; this.isMultiThreadedEnbled = isMultiThreaded; @@ -50,6 +56,7 @@ private RaptorRequestMapper( this.egressPaths = egressPaths; this.transitSearchTimeZeroEpocSecond = transitSearchTimeZeroEpocSecond; this.meterRegistry = meterRegistry; + this.transitLayer = transitLayer; } public static RaptorRequest mapRequest( @@ -58,7 +65,8 @@ public static RaptorRequest mapRequest( boolean isMultiThreaded, Collection accessPaths, Collection egressPaths, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + TransitLayer transitLayer ) { return new RaptorRequestMapper( request, @@ -66,7 +74,8 @@ public static RaptorRequest mapRequest( accessPaths, egressPaths, transitSearchTimeZero.toEpochSecond(), - meterRegistry + meterRegistry, + transitLayer ) .doMap(); } @@ -176,7 +185,7 @@ private RaptorRequest doMap() { ); } - return builder.build(); + return EnturHackSorlandsBanen.enableHack(builder.build(), request, transitLayer); } private List mapPassThroughPoints() { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java index 8c310206a01..d4785dc6f5b 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java @@ -4,6 +4,7 @@ import java.util.BitSet; import java.util.Iterator; import java.util.List; +import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.opentripplanner.framework.application.OTPFeature; @@ -27,6 +28,8 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.constrainedtransfer.ConstrainedBoardingSearch; import org.opentripplanner.routing.algorithm.raptoradapter.transit.constrainedtransfer.ConstrainedTransfersForPatterns; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.CostCalculatorFactory; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.DefaultCostCalculator; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.FactorStrategy; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.GeneralizedCostParametersMapper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.transit.model.network.RoutingTripPattern; @@ -253,4 +256,34 @@ private PriorityGroupConfigurator createTransitGroupPriorityConfigurator(RouteRe transitRequest.priorityGroupsGlobal() ); } + + /*-- HACK SØRLANDSBANEN :: BEGIN --*/ + + private RaptorRoutingRequestTransitData( + RaptorRoutingRequestTransitData original, + Function mapFactors + ) { + this.transitLayer = original.transitLayer; + this.transitSearchTimeZero = original.transitSearchTimeZero; + this.activeTripPatternsPerStop = original.activeTripPatternsPerStop; + this.patternIndex = original.patternIndex; + this.transferIndex = original.transferIndex; + this.transferService = original.transferService; + this.constrainedTransfers = original.constrainedTransfers; + this.validTransitDataStartTime = original.validTransitDataStartTime; + this.validTransitDataEndTime = original.validTransitDataEndTime; + this.generalizedCostCalculator = + new DefaultCostCalculator<>( + (DefaultCostCalculator) original.generalizedCostCalculator, + mapFactors + ); + this.slackProvider = original.slackProvider(); + } + + public RaptorTransitDataProvider enturHackSorlandsbanen( + Function mapFactors + ) { + return new RaptorRoutingRequestTransitData(this, mapFactors); + } + /*-- HACK SØRLANDSBANEN :: END --*/ } diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java index 78f30277e72..94a4a458915 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/TransitPreferences.java @@ -31,6 +31,7 @@ public final class TransitPreferences implements Serializable { private final boolean includePlannedCancellations; private final boolean includeRealtimeCancellations; private final RaptorPreferences raptor; + private final double extraSearchCoachReluctance; private TransitPreferences() { this.boardSlack = this.alightSlack = DurationForEnum.of(TransitMode.class).build(); @@ -42,6 +43,7 @@ private TransitPreferences() { this.includePlannedCancellations = false; this.includeRealtimeCancellations = false; this.raptor = RaptorPreferences.DEFAULT; + this.extraSearchCoachReluctance = 0.0; } private TransitPreferences(Builder builder) { @@ -55,6 +57,7 @@ private TransitPreferences(Builder builder) { this.includePlannedCancellations = builder.includePlannedCancellations; this.includeRealtimeCancellations = builder.includeRealtimeCancellations; this.raptor = requireNonNull(builder.raptor); + this.extraSearchCoachReluctance = builder.extraSearchCoachReluctance; } public static Builder of() { @@ -165,6 +168,11 @@ public RaptorPreferences raptor() { return raptor; } + /** Zero means turned off. HACK SØRLANDSBANEN */ + public double extraSearchCoachReluctance() { + return extraSearchCoachReluctance; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -180,6 +188,7 @@ public boolean equals(Object o) { ignoreRealtimeUpdates == that.ignoreRealtimeUpdates && includePlannedCancellations == that.includePlannedCancellations && includeRealtimeCancellations == that.includeRealtimeCancellations && + extraSearchCoachReluctance == that.extraSearchCoachReluctance && raptor.equals(that.raptor) ); } @@ -196,6 +205,7 @@ public int hashCode() { ignoreRealtimeUpdates, includePlannedCancellations, includeRealtimeCancellations, + extraSearchCoachReluctance, raptor ); } @@ -245,6 +255,7 @@ public static class Builder { private boolean includePlannedCancellations; private boolean includeRealtimeCancellations; private RaptorPreferences raptor; + private double extraSearchCoachReluctance; public Builder(TransitPreferences original) { this.original = original; @@ -258,6 +269,7 @@ public Builder(TransitPreferences original) { this.includePlannedCancellations = original.includePlannedCancellations; this.includeRealtimeCancellations = original.includeRealtimeCancellations; this.raptor = original.raptor; + this.extraSearchCoachReluctance = original.extraSearchCoachReluctance; } public TransitPreferences original() { @@ -327,6 +339,11 @@ public Builder withRaptor(Consumer body) { return this; } + public Builder setExtraSearchCoachReluctance(double extraSearchCoachReluctance) { + this.extraSearchCoachReluctance = extraSearchCoachReluctance; + return this; + } + public Builder apply(Consumer body) { body.accept(this); return this; diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index f9ea136fbe2..df1ee7bb0eb 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -316,13 +316,14 @@ The board time is added to the time when going from the stop (offboard) to onboa } // TODO REMOVE THIS - builder.withRaptor(it -> - c - .of("relaxTransitSearchGeneralizedCostAtDestination") - .since(V2_3) - .summary("Whether non-optimal transit paths at the destination should be returned") - .description( - """ + builder + .withRaptor(it -> + c + .of("relaxTransitSearchGeneralizedCostAtDestination") + .since(V2_3) + .summary("Whether non-optimal transit paths at the destination should be returned") + .description( + """ Let c be the existing minimum pareto optimal generalized cost to beat. Then a trip with cost c' is accepted if the following is true: `c' < Math.round(c * relaxRaptorCostCriteria)`. @@ -332,10 +333,17 @@ The board time is added to the time when going from the stop (offboard) to onboa Values equals or less than zero is not allowed. Values greater than 2.0 are not supported, due to performance reasons. """ - ) - .asDoubleOptional() - .ifPresent(it::withRelaxGeneralizedCostAtDestination) - ); + ) + .asDoubleOptional() + .ifPresent(it::withRelaxGeneralizedCostAtDestination) + ) + .setExtraSearchCoachReluctance( + c + .of("extraSearchCoachReluctance") + .since(V2_1) + .summary("TODO") + .asDouble(dft.extraSearchCoachReluctance()) + ); } private static void mapBikePreferences(NodeAdapter root, BikePreferences.Builder builder) { diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index fbaa2418d7e..3462c67e458 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -805,6 +805,8 @@ type QueryType { dateTime: DateTime, "Debug the itinerary-filter-chain. OTP will attach a system notice to itineraries instead of removing them. This is very convenient when tuning the filters." debugItineraryFilter: Boolean = false @deprecated(reason : "Use `itineraryFilter.debug` instead."), + "FOR TESTING ONLY" + extraSearchCoachReluctance: Float, "A list of filters for which trips should be included. A trip will be included if it matches with at least one filter. An empty list of filters means that all trips should be included. If a search include this parameter, \"whiteListed\", \"banned\" & \"modes.transportModes\" filters will be ignored." filters: [TripFilterInput!], "The start location" diff --git a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java index 39022da42ca..aeca5a36ebf 100644 --- a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java +++ b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java @@ -39,6 +39,9 @@ public class RaptorArchitectureTest { private static final Package RR_STANDARD = RANGE_RAPTOR.subPackage("standard"); private static final Package RR_STD_CONFIGURE = RR_STANDARD.subPackage("configure"); private static final Package RR_CONTEXT = RANGE_RAPTOR.subPackage("context"); + private static final Package EXT_SORLANDSBANAN_HACK = Package.of( + "org.opentripplanner.ext.sorlandsbanen" + ); /** * Packages used by standard-range-raptor and multi-criteria-range-raptor. @@ -200,7 +203,8 @@ void enforcePackageDependenciesInRaptorService() { RAPTOR_UTIL, CONFIGURE, RR_INTERNAL_API, - RR_TRANSIT + RR_TRANSIT, + EXT_SORLANDSBANAN_HACK ) .verify(); } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java index 47542782884..e3fbbc0df21 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java @@ -90,6 +90,7 @@ private static RaptorRequest map(RouteRequest request) { false, ACCESS, EGRESS, + null, null ); } From 12885db7c5409182e9bafb6f81389f847c82e885 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 14 Jun 2024 09:27:04 +0200 Subject: [PATCH 15/20] Fix too large response cancellation --- .../restapi/resources/PlannerResource.java | 5 ++-- .../opentripplanner/api/common/Message.java | 2 +- .../MaxFieldsInResultInstrumentation.java | 3 +-- .../transmodel/ResponseTooLargeException.java | 11 ++++++++ .../apis/transmodel/TransmodelGraph.java | 6 +++-- ...nprocessableRequestExecutionStrategy.java} | 22 ++++++++++------ .../support/ExecutionResultMapper.java | 26 ++++++++++++++----- src/main/resources/Message.properties | 2 +- src/main/resources/Message_de.properties | 2 +- src/main/resources/Message_es.properties | 2 +- src/main/resources/Message_fr.properties | 2 +- src/main/resources/Message_hu.properties | 2 +- src/main/resources/Message_nl.properties | 2 +- .../support/ExecutionResultMapperTest.java | 22 ++++++++++++++++ 14 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java rename src/main/java/org/opentripplanner/apis/transmodel/support/{AbortOnTimeoutExecutionStrategy.java => AbortOnUnprocessableRequestExecutionStrategy.java} (62%) diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java index 38c70851b76..9c58c1c719d 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/PlannerResource.java @@ -12,6 +12,7 @@ import org.opentripplanner.api.common.Message; import org.opentripplanner.api.error.PlannerError; import org.opentripplanner.apis.support.mapping.PlannerErrorMapper; +import org.opentripplanner.apis.transmodel.ResponseTooLargeException; import org.opentripplanner.ext.restapi.mapping.TripPlanMapper; import org.opentripplanner.ext.restapi.mapping.TripSearchMetadataMapper; import org.opentripplanner.ext.restapi.model.ElevationMetadata; @@ -104,8 +105,8 @@ public Response plan(@Context UriInfo uriInfo, @Context Request grizzlyRequest) LOG.error("System error - unhandled error case?", e); response.setError(new PlannerError(Message.SYSTEM_ERROR)); } - } catch (OTPRequestTimeoutException e) { - response.setError(new PlannerError(Message.PROCESSING_TIMEOUT)); + } catch (OTPRequestTimeoutException | ResponseTooLargeException e) { + response.setError(new PlannerError(Message.UNPROCESSABLE_REQUEST)); } catch (Exception e) { LOG.error("System error", e); response.setError(new PlannerError(Message.SYSTEM_ERROR)); diff --git a/src/main/java/org/opentripplanner/api/common/Message.java b/src/main/java/org/opentripplanner/api/common/Message.java index 9a07d8c33d6..3e568d57940 100644 --- a/src/main/java/org/opentripplanner/api/common/Message.java +++ b/src/main/java/org/opentripplanner/api/common/Message.java @@ -15,7 +15,7 @@ public enum Message { PLAN_OK(200), SYSTEM_ERROR(500), - PROCESSING_TIMEOUT(422), + UNPROCESSABLE_REQUEST(422), GRAPH_UNAVAILABLE(503), diff --git a/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java b/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java index 6d22f18783c..7cc356e5f6b 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/MaxFieldsInResultInstrumentation.java @@ -3,7 +3,6 @@ import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; import graphql.ExecutionResult; -import graphql.execution.AbortExecutionException; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; @@ -47,7 +46,7 @@ public InstrumentationContext beginFieldFetch( if (fetched % 10000 == 0) { LOG.debug("Fetched {} fields", fetched); if (fetched > maxFieldFetch) { - throw new AbortExecutionException( + throw new ResponseTooLargeException( "The number of fields in the GraphQL result exceeds the maximum allowed: " + maxFieldFetch ); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java b/src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java new file mode 100644 index 00000000000..a6b070e8fdd --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/ResponseTooLargeException.java @@ -0,0 +1,11 @@ +package org.opentripplanner.apis.transmodel; + +/** + * Exception thrown when the API response exceeds a configurable limit. + */ +public class ResponseTooLargeException extends RuntimeException { + + public ResponseTooLargeException(String message) { + super(message); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java index 1bff91638fd..d755c509989 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java @@ -16,7 +16,7 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.opentripplanner.apis.transmodel.support.AbortOnTimeoutExecutionStrategy; +import org.opentripplanner.apis.transmodel.support.AbortOnUnprocessableRequestExecutionStrategy; import org.opentripplanner.apis.transmodel.support.ExecutionResultMapper; import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation; import org.opentripplanner.framework.application.OTPFeature; @@ -50,7 +50,7 @@ Response executeGraphQL( int maxNumberOfResultFields, Iterable tracingTags ) { - try (var executionStrategy = new AbortOnTimeoutExecutionStrategy()) { + try (var executionStrategy = new AbortOnUnprocessableRequestExecutionStrategy()) { variables = ObjectUtils.ifNotNull(variables, new HashMap<>()); var instrumentation = createInstrumentation(maxNumberOfResultFields, tracingTags); var transmodelRequestContext = createRequestContext(serverContext); @@ -69,6 +69,8 @@ Response executeGraphQL( return ExecutionResultMapper.okResponse(result); } catch (OTPRequestTimeoutException te) { return ExecutionResultMapper.timeoutResponse(); + } catch (ResponseTooLargeException rtle) { + return ExecutionResultMapper.tooLargeResponse(rtle.getMessage()); } catch (CoercingParseValueException | UnknownOperationException e) { return ExecutionResultMapper.badRequestResponse(e.getMessage()); } catch (Exception systemError) { diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java b/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnUnprocessableRequestExecutionStrategy.java similarity index 62% rename from src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java rename to src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnUnprocessableRequestExecutionStrategy.java index a925aff0a1d..a8a664fc18d 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnUnprocessableRequestExecutionStrategy.java @@ -5,22 +5,28 @@ import graphql.schema.DataFetchingEnvironment; import java.io.Closeable; import java.util.concurrent.CompletableFuture; +import org.opentripplanner.apis.transmodel.ResponseTooLargeException; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.framework.logging.ProgressTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * To abort fetching data when a timeout occurs we have to rethrow the time-out-exception. + * To abort fetching data when a request is unprocessable (either because the execution times + * out or because the response is too large) we have to rethrow the exception. * This will prevent unresolved data-fetchers to be called. The exception is not handled * gracefully. */ -public class AbortOnTimeoutExecutionStrategy extends AsyncExecutionStrategy implements Closeable { +public class AbortOnUnprocessableRequestExecutionStrategy + extends AsyncExecutionStrategy + implements Closeable { - private static final Logger LOG = LoggerFactory.getLogger(AbortOnTimeoutExecutionStrategy.class); + private static final Logger LOG = LoggerFactory.getLogger( + AbortOnUnprocessableRequestExecutionStrategy.class + ); public static final int LOG_STEPS = 25_000; private final ProgressTracker timeoutProgressTracker = ProgressTracker.track( - "TIMEOUT! Abort GraphQL query", + "Unprocessable request. Abort GraphQL query", LOG_STEPS, -1 ); @@ -31,15 +37,15 @@ protected CompletableFuture handleFetchingException( ExecutionStrategyParameters params, Throwable e ) { - if (e instanceof OTPRequestTimeoutException te) { - logTimeoutProgress(); - throw te; + if (e instanceof OTPRequestTimeoutException || e instanceof ResponseTooLargeException) { + logCancellationProgress(); + throw (RuntimeException) e; } return super.handleFetchingException(environment, params, e); } @SuppressWarnings("Convert2MethodRef") - private void logTimeoutProgress() { + private void logCancellationProgress() { timeoutProgressTracker.startOrStep(m -> LOG.info(m)); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java index d0ccb198e16..aac9cb1bb7c 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapper.java @@ -16,6 +16,11 @@ public class ExecutionResultMapper { private static final ErrorClassification API_PROCESSING_TIMEOUT = ErrorClassification.errorClassification( "ApiProcessingTimeout" ); + + private static final ErrorClassification RESPONSE_TOO_LARGE = ErrorClassification.errorClassification( + "ResponseTooLarge" + ); + private static final ErrorClassification BAD_REQUEST_ERROR = ErrorClassification.errorClassification( "BadRequestError" ); @@ -29,13 +34,11 @@ public static Response okResponse(ExecutionResult result) { } public static Response timeoutResponse() { - var error = GraphQLError - .newError() - .errorType(API_PROCESSING_TIMEOUT) - .message(OTPRequestTimeoutException.MESSAGE) - .build(); - var result = ExecutionResult.newExecutionResult().addError(error).build(); - return response(result, OtpHttpStatus.STATUS_UNPROCESSABLE_ENTITY); + return unprocessableResponse(API_PROCESSING_TIMEOUT, OTPRequestTimeoutException.MESSAGE); + } + + public static Response tooLargeResponse(String message) { + return unprocessableResponse(RESPONSE_TOO_LARGE, message); } public static Response badRequestResponse(String message) { @@ -56,4 +59,13 @@ public static Response response(ExecutionResult result, Response.StatusType stat .entity(GraphQLResponseSerializer.serialize(result)) .build(); } + + private static Response unprocessableResponse( + ErrorClassification errorClassification, + String message + ) { + var error = GraphQLError.newError().errorType(errorClassification).message(message).build(); + var result = ExecutionResult.newExecutionResult().addError(error).build(); + return response(result, OtpHttpStatus.STATUS_UNPROCESSABLE_ENTITY); + } } diff --git a/src/main/resources/Message.properties b/src/main/resources/Message.properties index 5c6011e3d3d..931b25c8b3b 100644 --- a/src/main/resources/Message.properties +++ b/src/main/resources/Message.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = We're sorry. The trip planner is temporarily unavailable. Please GRAPH_UNAVAILABLE = We're sorry. The trip planner is temporarily unavailable. Please try again later. OUTSIDE_BOUNDS = Trip is not possible. You might be trying to plan a trip outside the map data boundary. -PROCESSING_TIMEOUT = The trip planner is taking too long to process your request. +UNPROCESSABLE_REQUEST = The trip planner is taking too long time or too much resources to process your request. BOGUS_PARAMETER = The request has errors that the server is not willing or able to process. LOCATION_NOT_ACCESSIBLE = The location was found, but no stops could be found within the search radius. PATH_NOT_FOUND = No trip found. There may be no transit service within the maximum specified distance or at the specified time, or your start or end point might not be safely accessible. diff --git a/src/main/resources/Message_de.properties b/src/main/resources/Message_de.properties index 1b0d8a2322e..6862a993c71 100644 --- a/src/main/resources/Message_de.properties +++ b/src/main/resources/Message_de.properties @@ -2,7 +2,7 @@ PLAN_OK = Success SYSTEM_ERROR = Es tut uns leid, leider steht der Trip-Planer momentan nicht zur Verfügung. Bitte versuchen Sie es zu einem späteren Zeitpunkt nochmal. OUTSIDE_BOUNDS = Planung nicht möglich. Vielleicht versuchen sie einen Plan außerhalb der Kartengrenzen zu planen. -PROCESSING_TIMEOUT = Der Trip-Planner braucht zu lange um die Anfrage zu bearbeiten +UNPROCESSABLE_REQUEST = Der Trip-Planner braucht zu lange um die Anfrage zu bearbeiten BOGUS_PARAMETER = Die Anfrage ist fehlerhaft so dass sie der Server nicht bearbeiten möchte oder kann. PATH_NOT_FOUND = Planung nicht möglich. Ihr Start- oder Endpunkt könnte nicht erreichbar sein. Bitte stellen sie sicher, dass ihre Anfrage innerhalb der Kartendaten ist. NO_TRANSIT_TIMES = Keine Fahrzeiten verfügbar. Das Datum kann zu weit in der Vergangenheit oder zu weit in der Zukunft liegen oder es gibt keinen Verkehrsbetrieb zu dem von Ihnen gewählten Zeitpunkt. diff --git a/src/main/resources/Message_es.properties b/src/main/resources/Message_es.properties index 0c5fe7a9395..88b7c849acb 100644 --- a/src/main/resources/Message_es.properties +++ b/src/main/resources/Message_es.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = ES-We're sorry. The trip planner is temporarily unavailable. Plea GRAPH_UNAVAILABLE = ES-We're sorry. The trip planner is temporarily unavailable. Please try again later. OUTSIDE_BOUNDS = ES-los trip is not possible. You might be trying to plan a trip outside the map data boundary. -PROCESSING_TIMEOUT = ES-los trip planner is taking too long to process your request. +UNPROCESSABLE_REQUEST = ES-los trip planner is taking too long time or too much resources to process your request. BOGUS_PARAMETER = ES-los request has errors that the server is not willing or able to process. PATH_NOT_FOUND = ES-los trip is not possible. Please check that you plan is within the bound of the map. NO_TRANSIT_TIMES = ES-Non transit times available. The date may be past or too far in the future or there may not be transit service for your trip at the time you chose. diff --git a/src/main/resources/Message_fr.properties b/src/main/resources/Message_fr.properties index 6f7b73c1997..66ff82bbc59 100644 --- a/src/main/resources/Message_fr.properties +++ b/src/main/resources/Message_fr.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = Désolé: le calculateur d'itinéraires est temporairement indisp GRAPH_UNAVAILABLE = Désolé: le calculateur d'itinéraires est temporairement indisponible. Veuillez recommencer ultérieurement. OUTSIDE_BOUNDS = Impossible de calculer un itinéraire. Vous essayez de planifier un itinéraire hors des limites de la zone couverte. -PROCESSING_TIMEOUT = Le calculateur d'itinéraires prend trop de temps pour gérer votre demande. +UNPROCESSABLE_REQUEST = Le calculateur d'itinéraires prend trop de temps ou de resources pour gérer votre demande. BOGUS_PARAMETER = Le serveur ne peut pas prendre en compte la requête, elle contient des paramètres erronés. PATH_NOT_FOUND = Impossible de calculer un itinéraire. Veuillez vérifier que le trajet demandé est inclus dans la zone couverte. NO_TRANSIT_TIMES = Aucun voyage disponible. Soit la date est trop loin dans le futur, soit il n'y a pas de service ce jour. diff --git a/src/main/resources/Message_hu.properties b/src/main/resources/Message_hu.properties index eca5fad9b10..f72f2295581 100644 --- a/src/main/resources/Message_hu.properties +++ b/src/main/resources/Message_hu.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = Sajn\u00E1ljuk. Az \u00FAtvonaltervez\u0151 \u00E1tmenetileg GRAPH_UNAVAILABLE = Sajn\u00E1ljuk. Az \u00FAtvonaltervez\u0151 \u00E1tmenetileg nem el\u00E9rhet\u0151. K\u00E9rj\u00FCk, pr\u00F3b\u00E1lja \u00FAjra k\u00E9s\u0151bb. OUTSIDE_BOUNDS = Az utaz\u00E1s nem lehets\u00E9ges. Lehet, hogy a t\u00E9rk\u00E9padat-hat\u00E1ron k\u00EDv\u00FCli utat pr\u00F3b\u00E1l megtervezni. -PROCESSING_TIMEOUT = Az utaz\u00E1stervez\u0151 t\u00FAl sok\u00E1ig tart a k\u00E9r\u00E9s feldolgoz\u00E1s\u00E1hoz. +UNPROCESSABLE_REQUEST = Az utaz\u00E1stervez\u0151 t\u00FAl sok\u00E1ig tart a k\u00E9r\u00E9s feldolgoz\u00E1s\u00E1hoz. BOGUS_PARAMETER = A k\u00E9r\u00E9s olyan hib\u00E1kat tartalmaz, amelyeket a szerver nem hajland\u00F3 vagy nem k\u00E9pes feldolgozni. LOCATION_NOT_ACCESSIBLE = A helyek megtal\u00E1lhat\u00F3ak, de meg\u00E1ll\u00F3k nem tal\u00E1lhat\u00F3 a keres\u00E9si k\u00F6rzetben. PATH_NOT_FOUND = Nem tal\u00E1lhat\u00F3 utaz\u00E1s. El\u0151fordulhat, hogy a megadott maxim\u00E1lis t\u00E1vols\u00E1gon bel\u00FCl vagy a megadott id\u0151pontban nincs t\u00F6megk\u00F6zleked\u00E9si szolg\u00E1ltat\u00E1s, vagy a kezd\u0151- vagy v\u00E9gpont nem \u00E9rhet\u0151 el biztons\u00E1gosan. diff --git a/src/main/resources/Message_nl.properties b/src/main/resources/Message_nl.properties index 1e18c998264..2a7a1b7700b 100644 --- a/src/main/resources/Message_nl.properties +++ b/src/main/resources/Message_nl.properties @@ -8,7 +8,7 @@ SYSTEM_ERROR = Onze excuses. De routeplanner is momenteel niet beschikbaar. Prob GRAPH_UNAVAILABLE = Onze excuses. De routeplanner is momenteel niet beschikbaar. Probeer later opnieuw. OUTSIDE_BOUNDS = Deze reis is niet mogelijk. Mogelijk probeert u een reis te plannen buiten het beschikbare gebied -PROCESSING_TIMEOUT = De routeplanner is te lang bezig met uw verzoek. +UNPROCESSABLE_REQUEST = De routeplanner is te lang bezig met uw verzoek. BOGUS_PARAMETER = Uw verzoek bevat fouten die de server niet wil or kan verwerken. PATH_NOT_FOUND = Reis is niet mogelijk. Misschien is het startpunt of de bestemming niet veilig toegankelijk. Bijvoorbeeld een straat alleen verbonden met een snelweg. NO_TRANSIT_TIMES = Geen OV-informatie beschikbaar. De datum is mogelijk te ver in het verleden of te ver in de toekomst. Of er is geen dienst voor uw reis op het moment dat u wilt. diff --git a/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java index bce971e9f1e..b697adc9288 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/support/ExecutionResultMapperTest.java @@ -34,6 +34,21 @@ class ExecutionResultMapperTest { "}" ); + public static final String TOO_LARGE_MESSAGE = + "The number of fields in the GraphQL result exceeds the maximum allowed: 100000"; + + private static final String TOO_LARGE_RESPONSE = quoteReplace( + "{'" + + "errors':[{" + + "'message':'" + + TOO_LARGE_MESSAGE + + "'," + + "'locations':[]," + + "'extensions':{'classification':'ResponseTooLarge'}" + + "}]" + + "}" + ); + public static final String SYSTEM_ERROR_MESSAGE = "A system error!"; public static final String SYSTEM_ERROR_RESPONSE = quoteReplace( @@ -62,6 +77,13 @@ void timeoutResponse() { assertEquals(TIMEOUT_RESPONSE, response.getEntity().toString()); } + @Test + void tooLargeResponse() { + var response = ExecutionResultMapper.tooLargeResponse(TOO_LARGE_MESSAGE); + assertEquals(422, response.getStatus()); + assertEquals(TOO_LARGE_RESPONSE, response.getEntity().toString()); + } + @Test void systemErrorResponse() { var response = ExecutionResultMapper.systemErrorResponse(SYSTEM_ERROR_MESSAGE); From 675e0b5216b29a37000ddf5b92243b4455146c63 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 14 Jun 2024 13:15:23 +0200 Subject: [PATCH 16/20] Version 2.6.0-entur-16 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 11818765947..05748f6d4b7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-SNAPSHOT + 2.6.0-entur-16 jar From cc058d8ee752a032bcbebe1ebc579a0f8942b9f0 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 14 Jun 2024 15:54:26 +0200 Subject: [PATCH 17/20] Add timePenalty to Transmodel API --- .../ext/restapi/mapping/ItineraryMapper.java | 5 +- .../transmodel/TransmodelGraphQLSchema.java | 9 ++- .../plan/TripPatternTimePenaltyType.java | 79 +++++++++++++++++++ .../model/plan/TripPatternType.java | 20 ++++- .../model/plan/TripPlanTimePenaltyDto.java | 32 ++++++++ .../apis/transmodel/schema.graphql | 38 +++++++++ .../plan/TripPlanTimePenaltyDtoTest.java | 67 ++++++++++++++++ 7 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java create mode 100644 src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java index d87cf33d71c..720d02fdd44 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java @@ -37,7 +37,10 @@ public ApiItinerary mapItinerary(Itinerary domain) { api.transitTime = domain.getTransitDuration().toSeconds(); api.waitingTime = domain.getWaitingDuration().toSeconds(); api.walkDistance = domain.getNonTransitDistanceMeters(); - api.generalizedCost = domain.getGeneralizedCost(); + // We list only the generalizedCostIncludingPenalty, this is the least confusing. We intend to + // delete this endpoint soon, so we will not make the proper change and add the + // generalizedCostIncludingPenalty to the response and update the debug client to show it. + api.generalizedCost = domain.getGeneralizedCostIncludingPenalty(); api.elevationLost = domain.getElevationLost(); api.elevationGained = domain.getElevationGained(); api.transfers = domain.getNumberOfTransfers(); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index 638d7783e9a..a88c36ac039 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -70,6 +70,7 @@ import org.opentripplanner.apis.transmodel.model.plan.PathGuidanceType; import org.opentripplanner.apis.transmodel.model.plan.PlanPlaceType; import org.opentripplanner.apis.transmodel.model.plan.RoutingErrorType; +import org.opentripplanner.apis.transmodel.model.plan.TripPatternTimePenaltyType; import org.opentripplanner.apis.transmodel.model.plan.TripPatternType; import org.opentripplanner.apis.transmodel.model.plan.TripQuery; import org.opentripplanner.apis.transmodel.model.plan.TripType; @@ -314,6 +315,7 @@ private GraphQLSchema create() { gqlUtil ); + GraphQLObjectType tripPatternTimePenaltyType = TripPatternTimePenaltyType.create(); GraphQLObjectType tripMetadataType = TripMetadataType.create(gqlUtil); GraphQLObjectType placeType = PlanPlaceType.create( bikeRentalStationType, @@ -339,7 +341,12 @@ private GraphQLSchema create() { elevationStepType, gqlUtil ); - GraphQLObjectType tripPatternType = TripPatternType.create(systemNoticeType, legType, gqlUtil); + GraphQLObjectType tripPatternType = TripPatternType.create( + systemNoticeType, + legType, + tripPatternTimePenaltyType, + gqlUtil + ); GraphQLObjectType routingErrorType = RoutingErrorType.create(); GraphQLOutputType tripType = TripType.create( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java new file mode 100644 index 00000000000..1a6e7310697 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java @@ -0,0 +1,79 @@ +package org.opentripplanner.apis.transmodel.model.plan; + +import graphql.Scalars; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLObjectType; +import org.opentripplanner.framework.time.DurationUtils; + +public class TripPatternTimePenaltyType { + + public static GraphQLObjectType create() { + return GraphQLObjectType + .newObject() + .name("TimePenalty") + .description( + """ + The time-penalty is applied to either the access-legs and/or egress-legs. Both access and + egress may contain more than one leg; Hence, the penalty is not a field on leg. + + Note! This is for debugging only. This type can change without notice. + """ + ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("appliedTo") + .description( + """ + The time-penalty is applied to either the access-legs and/or egress-legs. Both access + and egress may contain more than one leg; Hence, the penalty is not a field on leg. The + `appliedTo` describe witch part of the itinerary that this instance applies to. + """ + ) + .type(Scalars.GraphQLString) + .dataFetcher(environment -> penalty(environment).appliesTo()) + .build() + ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("timePenalty") + .description( + """ + The time-penalty added to the actual time/duration when comparing the itinerary with + other itineraries. This is used to decide witch is the best option, but is not visible + - the actual departure and arrival-times are not modified. + """ + ) + .type(Scalars.GraphQLString) + .dataFetcher(environment -> + DurationUtils.durationToStr(penalty(environment).penalty().time()) + ) + .build() + ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("generalizedCostPenalty") + .description( + """ + The time-penalty does also propagate to the `generalizedCost` But, while the + arrival-/departure-times listed is not affected, the generalized-cost is. In some cases + the time-penalty-cost is excluded when comparing itineraries - that happens if one of + the itineraries is a "direct/street-only" itinerary. Time-penalty can not be set for + direct searches, so it needs to be excluded from such comparison to be fair. The unit + is transit-seconds. + """ + ) + .type(Scalars.GraphQLInt) + .dataFetcher(environment -> penalty(environment).penalty().cost().toSeconds()) + .build() + ) + .build(); + } + + static TripPlanTimePenaltyDto penalty(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java index 2238a39c139..c903016b91b 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java @@ -16,6 +16,7 @@ public class TripPatternType { public static GraphQLObjectType create( GraphQLOutputType systemNoticeType, GraphQLObjectType legType, + GraphQLObjectType timePenaltyType, GqlUtil gqlUtil ) { return GraphQLObjectType @@ -189,7 +190,7 @@ public static GraphQLObjectType create( .name("generalizedCost") .description("Generalized cost or weight of the itinerary. Used for debugging.") .type(Scalars.GraphQLInt) - .dataFetcher(env -> itinerary(env).getGeneralizedCost()) + .dataFetcher(env -> itinerary(env).getGeneralizedCostIncludingPenalty()) .build() ) .field( @@ -228,6 +229,23 @@ public static GraphQLObjectType create( .dataFetcher(env -> itinerary(env).getTransferPriorityCost()) .build() ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("timePenalty") + .description( + """ + A time and cost penalty applied to access and egress to favor regular scheduled + transit over potentially faster options with FLEX, Car, bike and scooter. + + Note! This field is meant for debugging only. The field can be removed without notice + in the future. + """ + ) + .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(timePenaltyType)))) + .dataFetcher(env -> TripPlanTimePenaltyDto.of(itinerary(env))) + .build() + ) .build(); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java new file mode 100644 index 00000000000..b834b711327 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java @@ -0,0 +1,32 @@ +package org.opentripplanner.apis.transmodel.model.plan; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.opentripplanner.framework.model.TimeAndCost; +import org.opentripplanner.model.plan.Itinerary; + +/** + * A simple data-transfer-object used to map from an itinerary to the API specific + * type. It is needed because we need to pass in the "appliedTo" field, which does not + * exist in the domain model. + */ +public record TripPlanTimePenaltyDto(String appliesTo, TimeAndCost penalty) { + static List of(Itinerary itinerary) { + // This check for null to be robust - in case of a mistake in the future. + // The check is redundant on purpose. + if (itinerary == null) { + return List.of(); + } + return Stream + .of(of("access", itinerary.getAccessPenalty()), of("egress", itinerary.getEgressPenalty())) + .filter(Objects::nonNull) + .toList(); + } + + static TripPlanTimePenaltyDto of(String appliedTo, TimeAndCost penalty) { + return penalty == null || penalty.isZero() + ? null + : new TripPlanTimePenaltyDto(appliedTo, penalty); + } +} diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 1b01b2c2276..5f4768f247f 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -1217,6 +1217,36 @@ type TimeAndDayOffset { time: Time } +""" +The time-penalty is applied to either the access-legs and/or egress-legs. Both access and +egress may contain more than one leg; Hence, the penalty is not a field on leg. + +Note! This is for debugging only. This type can change without notice. +""" +type TimePenalty { + """ + The time-penalty is applied to either the access-legs and/or egress-legs. Both access + and egress may contain more than one leg; Hence, the penalty is not a field on leg. The + `appliedTo` describe witch part of the itinerary that this instance applies to. + """ + appliedTo: String + """ + The time-penalty does also propagate to the `generalizedCost` But, while the + arrival-/departure-times listed is not affected, the generalized-cost is. In some cases + the time-penalty-cost is excluded when comparing itineraries - that happens if one of + the itineraries is a "direct/street-only" itinerary. Time-penalty can not be set for + direct searches, so it needs to be excluded from such comparison to be fair. The unit + is transit-seconds. + """ + generalizedCostPenalty: Int + """ + The time-penalty added to the actual time/duration when comparing the itinerary with + other itineraries. This is used to decide witch is the best option, but is not visible + - the actual departure and arrival-times are not modified. + """ + timePenalty: String +} + "Scheduled passing times. These are not affected by real time updates." type TimetabledPassingTime { "Scheduled time of arrival at quay" @@ -1311,6 +1341,14 @@ type TripPattern { streetDistance: Float "Get all system notices." systemNotices: [SystemNotice!]! + """ + A time and cost penalty applied to access and egress to favor regular scheduled + transit over potentially faster options with FLEX, Car, bike and scooter. + + Note! This field is meant for debugging only. The field can be removed without notice + in the future. + """ + timePenalty: [TimePenalty!]! "A cost calculated to favor transfer with higher priority. This field is meant for debugging only." transferPriorityCost: Int "A cost calculated to distribute wait-time and avoid very short transfers. This field is meant for debugging only." diff --git a/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java new file mode 100644 index 00000000000..9ea6016324b --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java @@ -0,0 +1,67 @@ +package org.opentripplanner.apis.transmodel.model.plan; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; +import org.opentripplanner.framework.model.TimeAndCost; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Place; +import org.opentripplanner.model.plan.TestItineraryBuilder; +import org.opentripplanner.transit.model._data.TransitModelForTest; + +class TripPlanTimePenaltyDtoTest { + + private static final TimeAndCost PENALTY = new TimeAndCost( + DurationUtils.duration("20m30s"), + Cost.costOfSeconds(21) + ); + + private final TransitModelForTest testModel = TransitModelForTest.of(); + private final Place placeA = Place.forStop(testModel.stop("A").build()); + private final Place placeB = Place.forStop(testModel.stop("B").build()); + + @Test + void testCreateFromSingeEntry() { + assertNull(TripPlanTimePenaltyDto.of("access", null)); + assertNull(TripPlanTimePenaltyDto.of("access", TimeAndCost.ZERO)); + assertEquals( + new TripPlanTimePenaltyDto("access", PENALTY), + TripPlanTimePenaltyDto.of("access", PENALTY) + ); + } + + @Test + void testCreateFromItineraryWithNoPenalty() { + var i = itinerary(); + assertEquals(List.of(), TripPlanTimePenaltyDto.of(null)); + assertEquals(List.of(), TripPlanTimePenaltyDto.of(i)); + } + + @Test + void testCreateFromItineraryWithAccess() { + var i = itinerary(); + i.setAccessPenalty(PENALTY); + assertEquals( + List.of(new TripPlanTimePenaltyDto("access", PENALTY)), + TripPlanTimePenaltyDto.of(i) + ); + } + + @Test + void testCreateFromItineraryWithEgress() { + var i = itinerary(); + i.setEgressPenalty(PENALTY); + assertEquals( + List.of(new TripPlanTimePenaltyDto("egress", PENALTY)), + TripPlanTimePenaltyDto.of(i) + ); + } + + private Itinerary itinerary() { + return TestItineraryBuilder.newItinerary(placeA).drive(100, 200, placeB).build(); + } +} From 78337524288da18f9d2ed3f551c336b8c5560693 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 14 Jun 2024 16:27:07 +0200 Subject: [PATCH 18/20] Version 2.6.0-entur-17 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a2ff4ae435c..0427891f01d 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-entur-11 + 2.6.0-entur-17 jar From 49d52da007ae0244d7ddf96ea6e18472723a4dec Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 14 Jun 2024 15:54:26 +0200 Subject: [PATCH 19/20] Add timePenalty to Transmodel API --- .../ext/restapi/mapping/ItineraryMapper.java | 5 +- .../transmodel/TransmodelGraphQLSchema.java | 9 ++- .../plan/TripPatternTimePenaltyType.java | 79 +++++++++++++++++++ .../model/plan/TripPatternType.java | 20 ++++- .../model/plan/TripPlanTimePenaltyDto.java | 32 ++++++++ .../apis/transmodel/schema.graphql | 38 +++++++++ .../plan/TripPlanTimePenaltyDtoTest.java | 67 ++++++++++++++++ 7 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java create mode 100644 src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java index d87cf33d71c..720d02fdd44 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java @@ -37,7 +37,10 @@ public ApiItinerary mapItinerary(Itinerary domain) { api.transitTime = domain.getTransitDuration().toSeconds(); api.waitingTime = domain.getWaitingDuration().toSeconds(); api.walkDistance = domain.getNonTransitDistanceMeters(); - api.generalizedCost = domain.getGeneralizedCost(); + // We list only the generalizedCostIncludingPenalty, this is the least confusing. We intend to + // delete this endpoint soon, so we will not make the proper change and add the + // generalizedCostIncludingPenalty to the response and update the debug client to show it. + api.generalizedCost = domain.getGeneralizedCostIncludingPenalty(); api.elevationLost = domain.getElevationLost(); api.elevationGained = domain.getElevationGained(); api.transfers = domain.getNumberOfTransfers(); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index 638d7783e9a..a88c36ac039 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -70,6 +70,7 @@ import org.opentripplanner.apis.transmodel.model.plan.PathGuidanceType; import org.opentripplanner.apis.transmodel.model.plan.PlanPlaceType; import org.opentripplanner.apis.transmodel.model.plan.RoutingErrorType; +import org.opentripplanner.apis.transmodel.model.plan.TripPatternTimePenaltyType; import org.opentripplanner.apis.transmodel.model.plan.TripPatternType; import org.opentripplanner.apis.transmodel.model.plan.TripQuery; import org.opentripplanner.apis.transmodel.model.plan.TripType; @@ -314,6 +315,7 @@ private GraphQLSchema create() { gqlUtil ); + GraphQLObjectType tripPatternTimePenaltyType = TripPatternTimePenaltyType.create(); GraphQLObjectType tripMetadataType = TripMetadataType.create(gqlUtil); GraphQLObjectType placeType = PlanPlaceType.create( bikeRentalStationType, @@ -339,7 +341,12 @@ private GraphQLSchema create() { elevationStepType, gqlUtil ); - GraphQLObjectType tripPatternType = TripPatternType.create(systemNoticeType, legType, gqlUtil); + GraphQLObjectType tripPatternType = TripPatternType.create( + systemNoticeType, + legType, + tripPatternTimePenaltyType, + gqlUtil + ); GraphQLObjectType routingErrorType = RoutingErrorType.create(); GraphQLOutputType tripType = TripType.create( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java new file mode 100644 index 00000000000..1a6e7310697 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java @@ -0,0 +1,79 @@ +package org.opentripplanner.apis.transmodel.model.plan; + +import graphql.Scalars; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLObjectType; +import org.opentripplanner.framework.time.DurationUtils; + +public class TripPatternTimePenaltyType { + + public static GraphQLObjectType create() { + return GraphQLObjectType + .newObject() + .name("TimePenalty") + .description( + """ + The time-penalty is applied to either the access-legs and/or egress-legs. Both access and + egress may contain more than one leg; Hence, the penalty is not a field on leg. + + Note! This is for debugging only. This type can change without notice. + """ + ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("appliedTo") + .description( + """ + The time-penalty is applied to either the access-legs and/or egress-legs. Both access + and egress may contain more than one leg; Hence, the penalty is not a field on leg. The + `appliedTo` describe witch part of the itinerary that this instance applies to. + """ + ) + .type(Scalars.GraphQLString) + .dataFetcher(environment -> penalty(environment).appliesTo()) + .build() + ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("timePenalty") + .description( + """ + The time-penalty added to the actual time/duration when comparing the itinerary with + other itineraries. This is used to decide witch is the best option, but is not visible + - the actual departure and arrival-times are not modified. + """ + ) + .type(Scalars.GraphQLString) + .dataFetcher(environment -> + DurationUtils.durationToStr(penalty(environment).penalty().time()) + ) + .build() + ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("generalizedCostPenalty") + .description( + """ + The time-penalty does also propagate to the `generalizedCost` But, while the + arrival-/departure-times listed is not affected, the generalized-cost is. In some cases + the time-penalty-cost is excluded when comparing itineraries - that happens if one of + the itineraries is a "direct/street-only" itinerary. Time-penalty can not be set for + direct searches, so it needs to be excluded from such comparison to be fair. The unit + is transit-seconds. + """ + ) + .type(Scalars.GraphQLInt) + .dataFetcher(environment -> penalty(environment).penalty().cost().toSeconds()) + .build() + ) + .build(); + } + + static TripPlanTimePenaltyDto penalty(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java index 2238a39c139..c903016b91b 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java @@ -16,6 +16,7 @@ public class TripPatternType { public static GraphQLObjectType create( GraphQLOutputType systemNoticeType, GraphQLObjectType legType, + GraphQLObjectType timePenaltyType, GqlUtil gqlUtil ) { return GraphQLObjectType @@ -189,7 +190,7 @@ public static GraphQLObjectType create( .name("generalizedCost") .description("Generalized cost or weight of the itinerary. Used for debugging.") .type(Scalars.GraphQLInt) - .dataFetcher(env -> itinerary(env).getGeneralizedCost()) + .dataFetcher(env -> itinerary(env).getGeneralizedCostIncludingPenalty()) .build() ) .field( @@ -228,6 +229,23 @@ public static GraphQLObjectType create( .dataFetcher(env -> itinerary(env).getTransferPriorityCost()) .build() ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("timePenalty") + .description( + """ + A time and cost penalty applied to access and egress to favor regular scheduled + transit over potentially faster options with FLEX, Car, bike and scooter. + + Note! This field is meant for debugging only. The field can be removed without notice + in the future. + """ + ) + .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(timePenaltyType)))) + .dataFetcher(env -> TripPlanTimePenaltyDto.of(itinerary(env))) + .build() + ) .build(); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java new file mode 100644 index 00000000000..b834b711327 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java @@ -0,0 +1,32 @@ +package org.opentripplanner.apis.transmodel.model.plan; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.opentripplanner.framework.model.TimeAndCost; +import org.opentripplanner.model.plan.Itinerary; + +/** + * A simple data-transfer-object used to map from an itinerary to the API specific + * type. It is needed because we need to pass in the "appliedTo" field, which does not + * exist in the domain model. + */ +public record TripPlanTimePenaltyDto(String appliesTo, TimeAndCost penalty) { + static List of(Itinerary itinerary) { + // This check for null to be robust - in case of a mistake in the future. + // The check is redundant on purpose. + if (itinerary == null) { + return List.of(); + } + return Stream + .of(of("access", itinerary.getAccessPenalty()), of("egress", itinerary.getEgressPenalty())) + .filter(Objects::nonNull) + .toList(); + } + + static TripPlanTimePenaltyDto of(String appliedTo, TimeAndCost penalty) { + return penalty == null || penalty.isZero() + ? null + : new TripPlanTimePenaltyDto(appliedTo, penalty); + } +} diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 3462c67e458..e22ded472a6 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -1217,6 +1217,36 @@ type TimeAndDayOffset { time: Time } +""" +The time-penalty is applied to either the access-legs and/or egress-legs. Both access and +egress may contain more than one leg; Hence, the penalty is not a field on leg. + +Note! This is for debugging only. This type can change without notice. +""" +type TimePenalty { + """ + The time-penalty is applied to either the access-legs and/or egress-legs. Both access + and egress may contain more than one leg; Hence, the penalty is not a field on leg. The + `appliedTo` describe witch part of the itinerary that this instance applies to. + """ + appliedTo: String + """ + The time-penalty does also propagate to the `generalizedCost` But, while the + arrival-/departure-times listed is not affected, the generalized-cost is. In some cases + the time-penalty-cost is excluded when comparing itineraries - that happens if one of + the itineraries is a "direct/street-only" itinerary. Time-penalty can not be set for + direct searches, so it needs to be excluded from such comparison to be fair. The unit + is transit-seconds. + """ + generalizedCostPenalty: Int + """ + The time-penalty added to the actual time/duration when comparing the itinerary with + other itineraries. This is used to decide witch is the best option, but is not visible + - the actual departure and arrival-times are not modified. + """ + timePenalty: String +} + "Scheduled passing times. These are not affected by real time updates." type TimetabledPassingTime { "Scheduled time of arrival at quay" @@ -1311,6 +1341,14 @@ type TripPattern { streetDistance: Float "Get all system notices." systemNotices: [SystemNotice!]! + """ + A time and cost penalty applied to access and egress to favor regular scheduled + transit over potentially faster options with FLEX, Car, bike and scooter. + + Note! This field is meant for debugging only. The field can be removed without notice + in the future. + """ + timePenalty: [TimePenalty!]! "A cost calculated to favor transfer with higher priority. This field is meant for debugging only." transferPriorityCost: Int "A cost calculated to distribute wait-time and avoid very short transfers. This field is meant for debugging only." diff --git a/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java new file mode 100644 index 00000000000..9ea6016324b --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java @@ -0,0 +1,67 @@ +package org.opentripplanner.apis.transmodel.model.plan; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; +import org.opentripplanner.framework.model.TimeAndCost; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Place; +import org.opentripplanner.model.plan.TestItineraryBuilder; +import org.opentripplanner.transit.model._data.TransitModelForTest; + +class TripPlanTimePenaltyDtoTest { + + private static final TimeAndCost PENALTY = new TimeAndCost( + DurationUtils.duration("20m30s"), + Cost.costOfSeconds(21) + ); + + private final TransitModelForTest testModel = TransitModelForTest.of(); + private final Place placeA = Place.forStop(testModel.stop("A").build()); + private final Place placeB = Place.forStop(testModel.stop("B").build()); + + @Test + void testCreateFromSingeEntry() { + assertNull(TripPlanTimePenaltyDto.of("access", null)); + assertNull(TripPlanTimePenaltyDto.of("access", TimeAndCost.ZERO)); + assertEquals( + new TripPlanTimePenaltyDto("access", PENALTY), + TripPlanTimePenaltyDto.of("access", PENALTY) + ); + } + + @Test + void testCreateFromItineraryWithNoPenalty() { + var i = itinerary(); + assertEquals(List.of(), TripPlanTimePenaltyDto.of(null)); + assertEquals(List.of(), TripPlanTimePenaltyDto.of(i)); + } + + @Test + void testCreateFromItineraryWithAccess() { + var i = itinerary(); + i.setAccessPenalty(PENALTY); + assertEquals( + List.of(new TripPlanTimePenaltyDto("access", PENALTY)), + TripPlanTimePenaltyDto.of(i) + ); + } + + @Test + void testCreateFromItineraryWithEgress() { + var i = itinerary(); + i.setEgressPenalty(PENALTY); + assertEquals( + List.of(new TripPlanTimePenaltyDto("egress", PENALTY)), + TripPlanTimePenaltyDto.of(i) + ); + } + + private Itinerary itinerary() { + return TestItineraryBuilder.newItinerary(placeA).drive(100, 200, placeB).build(); + } +} From ab35129e0bef3575b25d241d8f74a545eabe5104 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 18 Jun 2024 13:50:42 +0200 Subject: [PATCH 20/20] Version 2.6.0-entur-18 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 05748f6d4b7..eddeeded25b 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0-entur-16 + 2.6.0-entur-18 jar