diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 94cc94f7179..f3de3aecfe4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,9 +4,10 @@ To be completed by pull request submitter: - [ ] **roadmap**: Check the [roadmap](https://github.com/orgs/opentripplanner/projects/1) for this feature or bug. If it is not already on the roadmap, PLC will discuss as part of the review process. - [ ] **tests**: Have you added relevant test coverage? Are all the tests passing on [the continuous integration service (Travis CI)](https://github.com/opentripplanner/OpenTripPlanner/blob/master/docs/Developers-Guide.md#continuous-integration)? - [ ] **formatting**: Have you followed the [suggested code style](https://github.com/opentripplanner/OpenTripPlanner/blob/master/docs/Developers-Guide.md#code-style)? +- [ ] **documentation**: If you are adding a new configuration option, have you added an explanation to the [configuration documentation](docs/Configuration.md) tables and sections? +- [ ] **changelog**: add a bullet point to the [changelog file](https://github.com/opentripplanner/OpenTripPlanner/blob/master/docs/Changelog.md) with description and link to the linked issue To be completed by @opentripplanner/plc: - [ ] reviews and approvals by 2 members, ideally from different organizations -- [ ] **before merging**: add a bullet point to the [changelog file](https://github.com/opentripplanner/OpenTripPlanner/blob/master/docs/Changelog.md) with description and link to the linked issue - [ ] **after merging**: update the relevant card on the [roadmap](https://github.com/orgs/opentripplanner/projects/1) \ No newline at end of file diff --git a/.gitignore b/.gitignore index b19351f67ed..db6be4ef580 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ gen-py/ _site/ /otp /otp-batch-analyst +*.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 862fa8f0f31..5e65ed8f290 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ dist: trusty # jdk 8 not available on xenial language: java -# OpenTripPlanner requires Java 8 and Travis doesn't (yet) support OpenJDK 8 + jdk: - - oraclejdk8 - + - openjdk8 + # Replace Travis's default Maven installation step with a no-op. # This avoids redundantly pre-running 'mvn install -DskipTests' every time. install: true @@ -34,7 +34,7 @@ after_success: # Secure envs are OSSRH_JIRA_USERNAME, OSSRH_JIRA_PASSWORD, GPG_KEY_NAME, GPG_PASSPHRASE env: global: - - JAVA_OPTS=-Xmx2g + - JAVA_OPTS=-Xmx2g # If sudo is disabled, CI runs on container based infrastructure (allows caching &c.) sudo: false @@ -72,4 +72,4 @@ deploy: skip_cleanup: true on: repo: ibi-group/OpenTripPlanner - all_branches: true \ No newline at end of file + all_branches: true diff --git a/README.md b/README.md index d4e95eeee49..f842cfd3ba4 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ After seven years of hard work and almost 10,000 commits from over 100 contribut ## Mailing Lists -The main forums through which the OpenTripPlanner community organizes development and provides mutual assistance are our two Google discussion groups. Changes and extensions to OTP are debated on the developers' list (opentripplanner-dev). More general questions and announcements of interest to non-developer OTP users should be directed to the opentripplanner-users list. +The main forums through which the OpenTripPlanner community organizes development and provides mutual assistance are our two Google discussion groups. Changes and extensions to OTP are debated on the developers' list - [opentripplanner-dev](https://groups.google.com/forum/#!forum/opentripplanner-dev). More general questions and announcements of interest to non-developer OTP users should be directed to the [opentripplanner-users](https://groups.google.com/forum/#!forum/opentripplanner-users) list. diff --git a/docs/Basic-Tutorial.md b/docs/Basic-Tutorial.md index 9491a8ab4ea..5ce992ed635 100644 --- a/docs/Basic-Tutorial.md +++ b/docs/Basic-Tutorial.md @@ -80,4 +80,4 @@ There are a number of different resources available through the HTTP API. Beside - Return all unique sequences of stops on the TriMet Green rail line: [http://localhost:8080/otp/routers/default/index/routes/TriMet:4/patterns](http://localhost:8080/otp/routers/default/index/routes/TriMet:4/patterns) -We refer to this as the Index API. It is also documented [in the OTP HTTP API docs](http://otp-docs.ibi-transit.com/api/resource_IndexAPI.html). +We refer to this as the Index API. It is also documented [in the OTP HTTP API docs](http://otp-docs.ibi-transit.com/api/resource_IndexAPI.html). \ No newline at end of file diff --git a/docs/Changelog.md b/docs/Changelog.md index 24b2df031c1..d8ec379b5c5 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,10 +1,12 @@ # Changelog -## 1.5 (in progress) +## 1.5.0 (in progress) +- Add application/x-protobuf to accepted protobuf content-types (#2839) - Add Way Property Set for the UK (#2818) - Fixes surefire test failure during build (#2816) - Improve documentation for `mode` routing parameter (#2809) +- Disable linking from already linked stops (#2372) ## 1.4 (2019-07-30) @@ -12,11 +14,28 @@ - Improved configuration documentation - Update onebusaway-gtfs to latest version from OBA project (#2636) - Remove the coupling to OneBusAway GTFS within OTP's internal model by creating new classes replacing the external classes (#2494) +- Allow OTP to search more service days for transit service (#2592) - Allow itineraries in response to be sorted by duration (#2593) - Add support for GTFS-flex services: flag stops, deviated-route service, and call-and-ride (#2603) - Fix reverse optimization bug (#2653, #2411) - Remove CarFreeAtoZ from list of deployments - Fix bike rented though no bikes/spaces are available (#2735) +- increase GTFS-realtime feeds size limit from 64MB to 2G (#2738) +- Fix XML response serialization (#2685) +- Refactor InterleavedBidirectionalHeuristic (#2671) +- Add "Accept" headers to GTFS-RT HTTP requests (#2796) +- Fix minor test failure against BANO geocoder (#2798) +- Fix frequency bounds checking (#2540) +- Fix JTS coordinate order for Polygons/Polylines (#2784) +- Add JAXB API to allow compilation under Java 11 +- Remove dependency on Conveyal jackson2-geojson +- Changed calculation of slope costs (#2579) +- Replace Java built in serialization with faster Kryo (#2681) +- Support OSM highway=razed tag (#2660) +- Memory leak fix (#2655) +- Add bicimad bike rental updater (#2503) +- Add Smoove citybikes updater (#2515) +- Switched to single license file, removing all OTP and OBA file license headers ## 1.3 (2018-08-03) diff --git a/docs/Configuration.md b/docs/Configuration.md index a211edc3ec6..d39a136e078 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -86,9 +86,10 @@ config key | description | value type | value default | notes `elevationBucket` | If specified, download NED elevation tiles from the given AWS S3 bucket | object | null | provide an object with `accessKey`, `secretKey`, and `bucketName` for AWS S3 `readCachedElevations` | If true, reads in pre-calculated elevation data. | boolean | true | see [Elevation Data Calculation Optimizations](#elevation-data-calculation-optimizations) `writeCachedElevations` | If true, writes the calculated elevation data. | boolean | false | see [Elevation Data Calculation Optimizations](#elevation-data-calculation-optimizations) +`elevationUnitMultiplier` | Specify a multiplier to convert elevation units from source to meters | double | 1.0 | see [Elevation unit conversion](#elevation-unit-conversion) `fares` | A specific fares service to use | object | null | see [fares configuration](#fares-configuration) `osmNaming` | A custom OSM namer to use | object | null | see [custom naming](#custom-naming) -`osmWayPropertySet` | Custom OSM way properties | string | `default` | options: `default`, `norway` +`osmWayPropertySet` | Custom OSM way properties | string | `default` | options: `default`, `norway`, `uk` `staticBikeRental` | Whether bike rental stations should be loaded from OSM, rather than periodically dynamically pulled from APIs | boolean | false | `staticParkAndRide` | Whether we should create car P+R stations from OSM data | boolean | true | `staticBikeParkAndRide` | Whether we should create bike P+R stations from OSM data | boolean | false | @@ -140,7 +141,6 @@ yield very realistic transfer time expectations. This works particularly well in the layering of non-intersecting ways is less prevalent. Here's an example in the Netherlands: View Larger Map - When such micro-mapping data is not available, we need to rely on information from GTFS including how stops are grouped into stations and a table of transfer timings where available. During the graph build, OTP can create preferential connections between each pair of stops in the same station to favor in-station transfers: @@ -244,6 +244,21 @@ order as the above-mentioned SRTM data, which is also the default for the popula DEM files(USGS DEM) is not supported by OTP, but can be converted to GeoTIFF with tools like [GDAL](http://www.gdal.org/). Use `gdal_merge.py -o merged.tiff *.dem` to merge a set of `dem` files into one `tif` file. +See Interline [PlanetUtils](https://github.com/interline-io/planetutils) for a set of scripts to download, merge, and resample [Mapzen/Amazon Terrain Tiles](https://registry.opendata.aws/terrain-tiles/). + +### Elevation unit conversion + +By default, OTP expects the elevation data to use metres. However, by setting `elevationUnitMultiplier` in `build-config.json`, +it is possible to define a multiplier that converts the elevation values from some other unit to metres. + +```JSON +// build-config.json +{ + // Correct conversation multiplier when source data uses decimetres instead of metres + "elevationUnitMultiplier": 0.1 +} +``` + ### Elevation Data Calculation Optimizations Calculating elevations on all StreetEdges can take a dramatically long time. In a very large graph build for multiple Northeast US states, the time it took to download the elevation data and calculate all of the elevations took 5,509 seconds (roughly 1.5 hours). @@ -333,10 +348,11 @@ It is possible to adjust how OSM data is interpreted by OpenTripPlanner when bui OSM tags have different meanings in different countries, and how the roads in a particular country or region are tagged affects routing. As an example are roads tagged with `highway=trunk (mainly) walkable in Norway, but forbidden in some other countries. This might lead to OTP being unable to snap stops to these roads, or by giving you poor routing results for walking and biking. You can adjust which road types that are accessible by foot, car & bicycle as well as speed limits, suitability for biking and walking. -There are currently 2 wayPropertySets defined; +There are currently 3 wayPropertySets defined; - `default` which is based on California/US mapping standard - `norway` which is adjusted to rules and speeds in Norway +- `uk` which is adjusted to rules and speed in the UK To add your own custom property set have a look at `org.opentripplanner.graph_builder.module.osm.NorwayWayPropertySet` and `org.opentripplanner.graph_builder.module.osm.DefaultWayPropertySet`. If you choose to mainly rely on the default rules, make sure you add your own rules first before applying the default ones. The mechanism is that for any two identical tags, OTP will use the first one. @@ -384,6 +400,15 @@ or MultiPolygons. If any part of the geometry of a StreetEdge intersects any par This section covers all options that can be set for each router using the `router-config.json` file. These options can be applied by the OTP server without rebuilding the graph. +config key | description | value type | value default | notes +---------- | ----------- | ---------- | ------------- | ----- +`routingDefaults` | Default routing parameters, which will be applied to every request | object | | see [routing defaults](#routing-defaults) +`timeout` | maximum time limit for route queries | double | null | units: seconds; see [timeouts](#timeouts) +`timeouts` | when returning multiple itineraries, set different maximum time limits for the 1st, 2nd, etc. itinerary | array of doubles | `[5, 4, 2]` | units: seconds; see [timeouts](#timeouts) +`requestLogFile` | Path to a plain-text file where requests will be logged | string | null | see [logging incoming requests](#logging-incoming-requests) +`boardTimes` | change boarding times by mode | object | null | see [boarding and alighting times](#boarding-and-alighting-times) +`alightTimes` | change alighting times by mode | object | null | see [boarding and alighting times](#boarding-and-alighting-times) +`updaters` | configure real-time updaters, such as GTFS-realtime feeds | object | null | see [configuring real-time updaters](#configuring-real-time-updaters) ## Routing defaults @@ -391,11 +416,7 @@ There are many trip planning options used in the OTP web API, and more exist internally that are not exposed via the API. You may want to change the default value for some of these parameters, i.e. the value which will be applied unless it is overridden in a web API request. -A full list of them can be found in the RoutingRequest class - -[in the Javadoc](http://otp-docs.ibi-transit.com/JavaDoc/org/opentripplanner/routing/core/RoutingRequest.html). - -Any public field or setter method in this class can be given a default value using the routingDefaults section of +A full list of them can be found in the RoutingRequest class [in the Javadoc](http://otp-docs.ibi-transit.com/JavaDoc/org/opentripplanner/routing/core/RoutingRequest.html). Any public field or setter method in this class can be given a default value using the routingDefaults section of `router-config.json` as follows: ```JSON @@ -413,8 +434,7 @@ Any public field or setter method in this class can be given a default value usi The routing request parameter `mode` determines which transport modalities should be considered when calculating the list of routes. -Some modes (mostly bicycle and car) also have optional qualifiers `RENT`, `HAIL` or `PARK` to specify if vehicles are to be parked at a station or rented. In theory -this can also apply to other modes but makes sense only in select cases which are listed below. +Some modes (mostly bicycle and car) also have optional qualifiers `RENT`, `HAIL` or `PARK` to specify if vehicles are to be parked at a station or rented. In theory this can also apply to other modes but makes sense only in select cases which are listed below. Whether a transport mode is available highly depends on the input feeds (GTFS, OSM, bike sharing feeds) and the graph building options supplied to OTP. @@ -637,7 +657,7 @@ The generic KML needs to be in format like ``` -### Configuration +### Configuring real-time updaters Real-time data can be provided using either a pull or push system. In a pull configuration, the GTFS-RT consumer polls the real-time provider over HTTP. That is to say, OTP fetches a file from a web server every few minutes. In the push @@ -724,15 +744,6 @@ connect to a network resource is the `url` field. // Streaming differential GTFS-RT TripUpdates over websockets { "type": "websocket-gtfs-rt-updater" - }, - - // OpenTraffic data - { - "type": "opentraffic-updater", - "frequencySec": -1, - // relative to OTP's working directory, where is traffic data stored. - // Should have subdirectories z/x/y.traffic.pbf (i.e. a tile tree of traffic tiles) - "tileDirectory": "traffic" } ] } diff --git a/docs/Deployments.md b/docs/Deployments.md index 70c975d404f..bca9d49259e 100644 --- a/docs/Deployments.md +++ b/docs/Deployments.md @@ -12,12 +12,12 @@ The following are known deployments of OTP in a government- or agency-sponsored * **Atlanta, Georgia** The Metropolitan Atlanta Rapid Transit Authority's (MARTA) [trip planner](http://itsmarta.com/planatrip.aspx) and the Atlanta region's transit information hub [atltransit.org](https://atltransit.org/) both use OTP to power their website trip planners. * **Boston, Massachusetts** The [Massachusetts Bay Transportation Authority trip planner](https://www.mbta.com/trip-planner). * **Seattle, Washington** The [Sound Transit Trip Planner](https://www.soundtransit.org/tripplanner) is based on OTP. OTP also powers the trip planning feature of the [OneBusAway native apps](http://onebusaway.org/) in the Puget Sound region. Technical details are [here](https://github.com/OneBusAway/onebusaway-android/blob/master/SYSTEM_ARCHITECTURE.md#add-trip-planning-andor-bike-share-optional). -* **Arlington, Virginia** The [commute planning site](http://www.carfreeatoz.com/) for the Washington, DC metropolitan area depends on OpenTripPlanner to weigh the costs and benefits of various travel options, making use of profile routing. * **Tampa, Florida** Hillsoborough Area Regional Transit uses an OpenTripPlanner server to power the trip planning feature of the [OneBusAway native apps](http://onebusaway.org/) in their region. Technical details are [here](https://github.com/OneBusAway/onebusaway-android/blob/master/SYSTEM_ARCHITECTURE.md#add-trip-planning-andor-bike-share-optional). * [**Piemonte Region, Italy**](https://map.muoversinpiemonte.it/#planner) and the [**City of Torino**](https://www.muoversiatorino.it/) built on OpenTripPlanner by [5T](http://www.5t.torino.it/). * [**Valencia, Spain**](http://www.emtvalencia.es/geoportal/?lang=en_otp) from the Municipal Transport Company of Valencia S.A.U. * [**Grenoble, France**](http://www.metromobilite.fr/) from SMTC, Grenoble Alpes métropole, l'État Français, the Rhône-alpes region, the Isère council and the City of Grenoble. * **Rennes, France** where the STAR network provides an OTP client for [iOS](https://itunes.apple.com/us/app/starbusmetro/id899970416?mt=8), [Android](https://play.google.com/store/apps/details?id=com.bookbeo.starbusmetro), Windows Phone et Web. +* **Alençon, France** integrated urban and school bus network [planner from Réunir Alençon](https://altobus.com/mon-itineraire/). * [**Poznań, Poland**](http://ztm.poznan.pl/#planner) from Urban Transport Authority of Poznań (ZTM Poznan). * **Trento Province, Italy** - [ViaggiaTrento](https://play.google.com/store/apps/details?id=eu.trentorise.smartcampus.viaggiatrento) and [ViaggiaRovereto](https://play.google.com/store/apps/details?id=eu.trentorise.smartcampus.viaggiarovereto) were implemented as part of the [SmartCampus Project](http://www.smartcampuslab.it), a research project founded by [TrentoRise](http://trentorise.eu), [UNITN](http://www.unitn.it), and [FBK](http://www.fbk.eu). diff --git a/docs/Developers-Guide.md b/docs/Developers-Guide.md index e01436b9258..e63e1a86d6c 100644 --- a/docs/Developers-Guide.md +++ b/docs/Developers-Guide.md @@ -46,6 +46,11 @@ There are several ways to get involved: * Submit patches. If you're not yet a committer, please provide patches as pull requests citing the relevant issue. Even when you do have push access to the repository, pull requests are a good way to get feedback on changes. +### Branches and Branch Protection + +As of January 2019, we have begun work on OTP 2.x and are using a Git branching model derived from [Gitflow](https://nvie.com/posts/a-successful-git-branching-model/). All development will occur on the `dev-1.x` and `dev-2.x` branches. Only release commits setting the Maven artifact version to a non-snapshot number should be pushed to the `master` branch of OTP. All other changes to master should result from fast-forward merges of a Github pull request from the `dev-1.x` branch. In turn, all changes to `dev-1.x` should result from a fast-forward merge of a Github pull request for a single feature, fix, or other change. These pull requests are subject to code review. We require two pull request approvals from OTP leadership committee members or designated code reviewers from two different organizations. We also have validation rules ensuring that the code compiles and all tests pass before pull requests can be merged. + +The `dev-2.x` branch is managed similarly to `dev-1.x` but because it's rapidly changing experimental code worked on by relatively few people, we require only one pull request approval from a different organization than the author. Merges will not occur into `master` from `dev-2.x` until that branch is sufficiently advanced and receives approval from the OTP project leadership committee. ### Issues and commits @@ -185,7 +190,7 @@ are not included in mainline OTP. OpenTripPlanner uses the same code formatting and style as the [GeoTools](http://www.geotools.org/) and [GeoServer](http://geoserver.org) projects. It's a minor variant of the -[Sun coding convention](http://www.oracle.com/technetwork/java/codeconv-138413.html). Notably, **we do not use tabs** +[Sun coding convention](https://www.oracle.com/technetwork/java/codeconventions-150003.pdf). Notably, **we do not use tabs** for indentation and we allow for lines up to 100 characters wide. The Eclipse formatter configuration supplied by the GeoTools project allows comments up to 150 characters wide. @@ -242,61 +247,125 @@ is pushed to the main OpenTripPlanner repository on GitHub, this server will com ## Release Process -This section serves as a checklist for the person responsible for performing releases (currently Andrew Byrd). -Note that much of this mimics the actions taken by the Maven release plugin. Based on past experience, -the release plugin can fail at various points in the process leaving the repo in a confusing state. -Taking each action manually is more annoying but keeps eyes on each step and is less prone to failure. +This section serves as a checklist for the person performing releases. Note that much of this mimics +the actions taken by the Maven release plugin. Based on past experience, the Maven release plugin can +fail at various points in the process leaving the repo in a confusing state. Taking each action +manually is more tedious, but keeps eyes on each step and is less prone to failure. Releases are +performed off the master branch, and are tagged with git annotated tags. -* Check that you are at the HEAD of master branch with no uncommitted changes +* Check that your local copy of the dev branch is up to date with no uncommitted changes * `git status` + * `git checkout dev-1.x` * `git clean -df` * `git pull` * Verify that all dependencies in the POM are non-SNAPSHOT versions + * Currently we do have one SNAPSHOT dependency on `crosby.binary.osmpbf` which we are working to eliminate * Update `docs/Changelog.md` - * Items should have been added/updated as each pull request was merged - * Update the header at the top of the list from x.y.z-SNAPSHOT to just x.y.z (current date) - * Add a new section header at the top of the list for the next iteration: x.y+1.0-SNAPSHOT + * Lines should have been added or updated as each pull request was merged + * If you suspect any changes are not reflected in the Changelog, review the commit log and add any missing items + * Update the header at the top of the list from `x.y.z-SNAPSHOT` to just `x.y.z (current date)` * Check in any changes, and push to Github * Check on Travis that the build is currently passing - * https://travis-ci.org/opentripplanner/OpenTripPlanner/builds + * [Link to OTP builds on Travis CI](https://travis-ci.org/opentripplanner/OpenTripPlanner/builds) +* Switch to the HEAD of master branch, and ensure it's up to date with no uncommitted changes + * `git checkout master` + * `git status` + * `git clean -df` + * `git pull` +* Merge the dev branch into master + * `git merge dev-1.x` * Bump the SNAPSHOT version in the POM to the release version - * Edit version in POM, removing SNAPSHOT and increasing version numbers as needed (semantic-release) + * Edit version in POM, removing SNAPSHOT and increasing version numbers as needed (following semantic versioning) * `git add pom.xml` * `git commit -m "prepare release x.y.z"` * Run a test build of the release locally, without deploying it - * `mvn clean package site` - * This will sign the Maven artifacts so you need the signing certificate set up + * `mvn clean install site` + * The `install` goal will sign the Maven artifacts so you need the GPG signing certificate set up + * You can also use the `package` goal instead of the `install` goal to avoid signing if you don't have the GPG certificate installed. * All tests should pass - * This will also create Enunciate API docs and Javadoc containing the correct non-snapshot version number -* Deploy documentation to AWS S3 + * This build will also create Enunciate API docs and Javadoc with the correct non-snapshot version number +* Deploy the documentation to AWS S3 * You have to do this right after the test release build to ensure the right version number in the docs * You will need AWSCLI tools (`sudo pip install -U awscli`) + * You will need AWS credentials with write access to the bucket `s3://dev.opentripplanner.org` * `aws s3 cp --recursive target/site/apidocs s3://dev.opentripplanner.org/javadoc/x.y.z --acl public-read` * `aws s3 cp --recursive target/site/enunciate/apidocs s3://dev.opentripplanner.org/apidoc/x.y.z --acl public-read` - * Check that docs are readable and show the correct version via the Javadoc landing page at `dev.opentripplanner.org` -* Finally, tag and push this release to make it official - * `git tag -a vX.Y.Z -m "release X.Y.X"` - * `git push origin vX.Y.Z` (will trigger a Travis CI build and deployment) -* Begin development on the next iteration - * Edit minor version in POM to X.(Y+1).0-SNAPSHOT - * `git add pom.xml` - * `git commit -m "Prepare next development iteration X.(Y+1).0-SNAPSHOT"` + * Check that docs are readable and show the correct version via the [development documentation landing page](http://dev.opentripplanner.org). +* Finally, if everything looks good, tag and push this release to make it official and trigger deployment + * `git tag -a vX.Y.Z -m "release X.Y.Z"` + * `git push origin vX.Y.Z` + * Pushing the tag will trigger a Travis CI build and deployment of release Maven artifacts + * Note that **only one** commit may have a particular non-snapshot version in the POM (this is the commit that + should be tagged as the release) +* Set up next development iteration + * Add a new section header to `docs/Changelog.md` like `x.y+1.0-SNAPSHOT (in progress)` + * Edit minor version in `pom.xml` to `x.y+1.0-SNAPSHOT` + * `git add pom.xml docs/Changelog.md` + * `git commit -m "Prepare next development iteration x.y+1.0-SNAPSHOT"` * `git push` * Check that Travis CI build of the release tag succeeded - * https://travis-ci.org/opentripplanner/OpenTripPlanner/builds - * Check the end of the build log to make sure the Maven artifacts were uploaded -* Check that Maven artifact appears on Maven Central - * OTP artifacts are at http://repo2.maven.org/maven2/org/opentripplanner/otp/1.3.0/ - * This can sometimes take half an hour after Travis uploads the artifacts + * [Link to OTP builds on Travis CI](https://travis-ci.org/opentripplanner/OpenTripPlanner/builds) + * Check the end of the build log to make sure the Maven artifacts were staged for release +* Check that Maven artifact appears on Maven Central (deployment succeeded) + * [Directory listing of OTP releases on Maven Central](https://repo1.maven.org/maven2/org/opentripplanner/otp/) + * It may take a while (half an hour) for releases to show up in the central repo after Travis uploads the artifacts +* Merge master back into dev (to sync up the Maven artifact version from the POM) + * `git checkout dev-1.x` + * `git merge master` + * `git push` +* Make sure the main documentation is built + * For some reason it doesn't always build automatically + * Go to [builds of docs.opentripplanner.org](http://readthedocs.org/projects/opentripplanner/builds/) + * Click "build version: latest" * Email the OTP dev and users mailing lists + * Mention the new version number. + * Provide links to the new developer documentation. + * Provide links to the artifacts directory on Maven Central. + * Trigger build of latest OTP documentation on Readthedocs. + +## Additional Information on Releases + +OpenTripPlanner is released as Maven artifacts to Maven Central. These include compiled and source code JARs as well as +a "shaded" JAR containing all dependencies, allowing stand-alone usage. This release process is handled by the +Sonatype Nexus Staging plugin, configured in the OpenTripPlanner POM. Typically this final Maven deployment action is +performed automatically when the Travis CI build succeeds in building a non-SNAPSHOT version. + +### Artifact Signing + +Maven release artifacts must be digitally signed to prove their origin. This is a safeguard against compromised code +from a malicious third party being disguised as a trusted library. + +The OTP artifact signing key was created by Conveyal. We export only that signing subkey, with our company's main key +blanked out. Therefore, even if someone managed to acquire the decrypted key file and the associated GPG passphrase, +they would not have the main key. We could deactivate the signing key and create a new one, without the main key being +compromised. + +The exported signing key is present in the root of the git repo as the encrypted file `maven-artifact-signing-key.asc.enc`. +When building a tagged release, Travis CI will decrypt this file and import it into GPG on the build machine. The signing +key ID and GPG passphrase are also present as encrypted environment variables in the Travis configuration YAML. This only +happens on code from non-fork, non-pull-request commits, ensuring that no unreviewed third-party code has access to +these files or variables. + +OpenTripPlanner's POM is set up to sign artifacts in the verify phase, which means signing will happen for the `install` +and `deploy` targets, but not the `package` target. +When performing a local test build, if you do `mvn clean install site` it will test the signing process. +If you do not have the certificate installed, you can instead to `mvn clean package site` to bypass signing, but this +provides less certainty that everything is set up correctly for the CI-driven final release. + +### Documentation Build and Hosting + +Three kinds of documentation are built for OTP, all based on information present in the OTP repo itself. + +The REST API docs are built by Enunciate from the OTP REST interface. My sense is that this auto-generated +documentation has become harder to read and less useful over time, perhaps because of incorrect handling of REST +parameters inherited from superclasses. + +The Javadoc is built from Javadoc comments in the source code itself. -TODO Elaborate on: -- Signing Certificate -- AWS IAM credentials -- Sonatype-OSS staging / Maven artifact deployment -- Merging dev branches into master ("git flow") -- Release from master should happen _automatically_ once version is changed -- ONLY one commit must have a particular non-snapshot commit (the tagged commit that constitutes the release) -- Add final steps: `git checkout dev-1.x`; `git merge master` (primarily to update the POM version) +The main OTP user documentation is built from Markdown files in the `/docs` directory of the repo. +The REST API docs and Javadoc are built by Maven, then uploaded manually to AWS S3, from which they are served as a web +page at dev.opentripplanner.org. The main OTP user documentation is built by Readthedocs and served at docs.opentripplanner.org. +Upload to the S3 bucket `dev.opentripplanner.org` requires AWS IAM credentials that can be created by Conveyal (which +owns the `dev.opentripplanner.org` bucket). \ No newline at end of file diff --git a/docs/Flex.md b/docs/Flex.md new file mode 100644 index 00000000000..b48bb6b0bb2 --- /dev/null +++ b/docs/Flex.md @@ -0,0 +1,70 @@ +# GTFS-Flex routing + +Many agencies run flexible services to complement their fixed-route service. "Flexible" service does +not follow a strict timetable or route. It may include any of the following features: boardings +or alightings outside its scheduled timetable and route; booking and scheduling in advance; or +transit parameters which depend on customer requests ("demand-responsive transit" or DRT). These +services are typically used in rural areas or for mobility-impaired riders. + +A GTFS extension called [GTFS-Flex](https://github.com/MobilityData/gtfs-flex/blob/master/spec/reference.md) defines +how to model some kinds of flexible transit. A subset of GTFS-Flex has been implemented in +OpenTripPlanner as part of US DOT's [Mobility-on-Demand Sandbox Grant](https://www.transit.dot.gov/research-innovation/fiscal-year-2016-mobility-demand-mod-sandbox-program-projects). + +In particular, OTP now has support for these modes of GTFS-Flex: + +- "flag stops", in which a passenger can flag down the a vehicle along its route to board, or +alight in between stops +- "deviated-route service", in which a vehicle can deviate from its route within an area or radius to +do a dropoff or pickup +- "call-and-ride", which is an entirely deviated, point-to-point segment. + +These modes can co-exist with fixed-route transit, and with each other. For example, some agencies +have fixed-route services that start in urban areas, where passengers must board at designated +stops, but end in rural areas where passengers can board and alight wherever they please. A +fixed-route service may terminate in an defined area where it can drop off passengers anywhere -- +or have such an area at the beginning or middle of its route. A vehicle may be able to deviate a +certain radius outside its scheduled route to pick up or drop off passengers. If both a pickup and +dropoff occur in between scheduled timepoints, from the passenger's perspective, the service may +look like a call-and-ride trip. Other call-and-ride services may operate more like taxis, in which +all rides are independently scheduled. + +## Configuration + +In order to use flexible routing, an OTP graph must be built with a GTFS-Flex dataset and +OpenStreetMap data. The GTFS data must include `shapes.txt`. + +In addition, the parameter `useFlexService: true` must be added to `router-config.json`. + +A number of routing parameters can be used to control aspects of flexible service. These parameters +typically change the relative cost of using various flexible services relative to fixed-route +transit. All flex-related parameters begin with the prefix "flex" and can be found in the Javadocs +for `RoutingRequest.java`. + +The following example `router-config.json` enables flexible routing and sets some parameters: + + { + "useFlexService": true, + "routingDefaults": { + "flexCallAndRideReluctance": 3, + "flexMaxCallAndRideSeconds": 7200, + "flexFlagStopExtraPenalty": 180 + } + } + +## Implementation + +The general approach of the GTFS-Flex implementation is as follows: prior to the main graph search, +special searches are run around the origin and destination to discover possible flexible options. +One search is with the WALK mode, to find flag stops, and the other is in the CAR mode, to find +deviated-route and call-and-ride options. These searches result in the creation of temporary, +request-specific vertices and edges. Then, the graph search proceeds as normal. Temporary graph +structures are disposed at the end of the request's lifecycle. + +For flag stops and deviated-route service, timepoints in between scheduled locations are determined +via linear interpolation. For example, say a particular trip departs stop A at 9:00am and arrives +at stop B at 9:30am. A passenger would be able to board 20% of the way in between stop A and stop B +at 9:06am, since 20% of 30 minutes is 6 minutes. + +For deviated-route service and call-and-ride service, the most pessimistic assumptions of vehicle +travel time are used -- e.g. vehicle travel time is calculated via the `drt_max_travel_time` +formula in the GTFS-Flex (see the spec [here](https://github.com/MobilityData/gtfs-flex/blob/master/spec/reference.md#defining-service-parameters)). \ No newline at end of file diff --git a/docs/Governance.md b/docs/Governance.md index 9c54a98bdef..d7cc14a7dfa 100644 --- a/docs/Governance.md +++ b/docs/Governance.md @@ -2,17 +2,19 @@ OpenTripPlanner is a member project of the [Software Freedom Conservancy](https://sfconservancy.org/members/current/). Development of OpenTripPlanner is managed by a Project Leadership Committee (PLC) which makes decisions by simple majority vote. The current members of this committee are (in alphabetical order): -|Name | Affiliation | -|-----|-------------| +| Name | Affiliation | +|-------------------|-----------------------------| | Sean Barbeau | University of South Florida | -| Torbjørn Barslett | Ruter Oslo | -| Sheldon Brown | Cambridge Systematics | -| Andrew Byrd | PlannerStack Foundation | -| Drew Dara-Abrams | Interline | -| David Emory | Conveyal | -| Tuukka Hastrup | Helsingin Seudun Liikenne | -| Frank Purcell | TriMet | -| David Turner | ex-OpenPlans | +| Sheldon Brown | Cambridge Systematics | +| Andrew Byrd | Conveyal | +| Thomas Craig | Trillium | +| Drew Dara-Abrams | Interline | +| David Emory | MARTA (Atlanta, Georgia) | +| Thomas Gran | Ruter & Entur (Norway) | +| Tuukka Hastrup | Maanteeamet (Estonia) | +| Frank Purcell | TriMet (Portland, Oregon) | +| Evan Siroky | IBI Group | +| David Turner | ex-OpenPlans | The PLC holds a quarterly video conference on the first Thursday of June, September, December, and March. An agenda is prepared as a collaborative document in advance of each quarterly meeting. These meetings are held at 9AM US Pacific time to accommodate members in the US Pacific, US Eastern, and Central European time zones. diff --git a/docs/Intermediate-Tutorial.md b/docs/Intermediate-Tutorial.md index 32fb1b0679c..6c2a2674bb8 100644 --- a/docs/Intermediate-Tutorial.md +++ b/docs/Intermediate-Tutorial.md @@ -17,7 +17,7 @@ The above query makes a request to the locally running server `http://localhost: - **mode=TRANSIT,WALK**, transport modes to consider, in this case a combination of walking and transit - **maxWalkDistance=500**, the maximum distance in meters that you are willing to walk -If you run this query as is you will very likely get a response saying that a trip has not been found. Try changing the fromPlace, toPlace, time and date parameters to match the location and time period of the data you loaded when you initially built the graph. More (optional) parameters for the planner resource are documented [here](http://otp-docs.ibi-transit.com/api/resource_PlannerResource.html). +If you run this query as is you will very likely get a response saying that a trip has not been found. Try changing the fromPlace, toPlace, time and date parameters to match the location and time period of the data you loaded when you initially built the graph. More (optional) parameters for the planner resource are documented [here](http://otp-docs.ibi-transit.com/api/resource_PlannerResource.html). ## Calculating travel time isochrones @@ -45,4 +45,4 @@ See the [configuration](Configuration.md) page for configuration settings which It may also be helpful to try running the OTP .jar file with the `--help` option for a full list of command line parameters. -Full documentation for the API is available [here](http://otp-docs.ibi-transit.com/api/index.html#resources). +Full documentation for the API is available [here](http://otp-docs.ibi-transit.com/api/index.html#resources). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 9eaf2fc5322..1766413ad42 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ _This documentation is targeted primarily at the OTP development community and m **OpenTripPlanner** (OTP) is an open source multi-modal trip planner, which runs on Linux, Mac, Windows, or potentially any platform with a Java virtual machine. OTP is released under the [LGPL license](https://opensource.org/licenses/LGPL-3.0). The code is under active development with a variety of [deployments](Deployments) around the world. -If you want to get started right away running your own OTP instance, the best place to start is the [Basic Usage](Basic-Usage) page. +If you want to get started right away running your own OTP instance, the best place to start is the [Basic Tutorial](Basic-Tutorial) page. ## External Technical Documentation diff --git a/mkdocs.yml b/mkdocs.yml index f66d1035095..4fc632c6a59 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,7 @@ pages: - Scripting: 'Scripting.md' - Security: 'Security.md' - Troubleshooting: 'Troubleshooting-Routing.md' + - GTFS-Flex Routing: 'Flex.md' - Development: - "Developers' Guide": 'Developers-Guide.md' - Architecture: 'Architecture.md' diff --git a/pom.xml b/pom.xml index 8353c365bab..5f51651385d 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ org.opentripplanner otp - 1.4.0-SNAPSHOT + 1.5.0-SNAPSHOT jar @@ -78,10 +78,11 @@ - 10.5 - 2.5.3 + 20.1 + 16.5 + 2.9.7 + 2.18 UTF-8 - 1.9.39 @@ -261,6 +262,7 @@ 2.21.0 -Xmx2G + -Dfile.encoding=UTF-8 false @@ -415,15 +417,10 @@ check central first to avoid a lot of not found warnings https://repo1.maven.org/maven2 - - download.java.net - Java.net Repository for Maven - http://download.java.net/maven/2/ - osgeo Open Source Geospatial Foundation Repository - http://download.osgeo.org/webdav/geotools/ + https://download.osgeo.org/webdav/geotools/ axis @@ -431,9 +428,9 @@ https://people.apache.org/repo/m1-ibiblio-rsync-repository/org.apache.axis2/ - sonatype-oss - Sonatype OSS - https://oss.sonatype.org/content/repositories/snapshots/ + sonatype-oss + Sonatype OSS + https://oss.sonatype.org/content/repositories/snapshots/ conveyal @@ -448,28 +445,6 @@ - - - com.amazonaws - aws-java-sdk-s3 - ${aws.version} - - - com.amazonaws - aws-java-sdk-ec2 - ${aws.version} - - - com.amazonaws - aws-java-sdk-sqs - ${aws.version} - - - - de.ruedigermoeller - fst - 2.34 - ch.qos.logback @@ -482,26 +457,6 @@ jul-to-slf4j 1.7.6 - - - - org.slf4j - jcl-over-slf4j - 1.7.6 - - - - com.google.guava - guava - 18.0 - - - - net.sf.trove4j - trove4j - 3.0.3 - - @@ -541,9 +496,14 @@ org.geotools - gt-wfs + gt-opengis ${geotools.version} + + org.geotools + gt-wfs + ${geotools.wfs.version} + org.geotools @@ -556,7 +516,7 @@ de.grundid.opendatalab geojson-jackson 1.2 - @@ -571,12 +531,6 @@ - - com.conveyal - jackson2-geojson - 0.8 - - junit @@ -598,33 +552,40 @@ test + + + + com.conveyal + kryo-tools + 1.2.0 + org.glassfish.jersey.core jersey-server - 2.18 + ${jersey.version} org.glassfish.jersey.media jersey-media-multipart - 2.18 + ${jersey.version} org.glassfish.jersey.containers jersey-container-grizzly2-http - 2.18 + ${jersey.version} - com.fasterxml.jackson.core - jackson-core + com.fasterxml.jackson.core + jackson-core ${jackson.version} - com.fasterxml.jackson.core - jackson-databind + com.fasterxml.jackson.core + jackson-databind ${jackson.version} @@ -719,7 +680,7 @@ org.apache.commons commons-compress - 1.17 + 1.18 commons-discovery @@ -756,9 +717,9 @@ 4.5.5 - commons-codec - commons-codec - 1.11 + commons-codec + commons-codec + 1.11 org.apache.commons @@ -792,45 +753,17 @@ graphql-java 2.2.0 + + + javax.xml.bind + jaxb-api + 2.3.1 + bsf bsf 2.4.0 - - - - io.opentraffic - traffic-engine - 0.2 - - - - com.conveyal - r5 - 4.1.0 - - - slf4j-simple - org.slf4j - - - - diff --git a/src/main/java/com/google/transit/realtime/GtfsRealtime.java b/src/main/java/com/google/transit/realtime/GtfsRealtime.java index 1f60cafd5bc..4c487832297 100644 --- a/src/main/java/com/google/transit/realtime/GtfsRealtime.java +++ b/src/main/java/com/google/transit/realtime/GtfsRealtime.java @@ -17,11 +17,13 @@ *

*
    *
  • Enum value TripDescriptor.ScheduleRelationship.MODIFIED = 5
  • + *
  • FeedMessage PARSER input.setSizeLimit(Integer.MAX_VALUE)
  • *
*

* Version 2.5.0 of protoc.exe has been used to generate this file. *

*/ + public final class GtfsRealtime { private GtfsRealtime() {} public static void registerAllExtensions( @@ -221,6 +223,10 @@ public FeedMessage parsePartialFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws com.google.protobuf.InvalidProtocolBufferException { + // Increase input stream size limit to 2G (instead of the 64M default). + // Mimics a change that was supposed to be released in protobuf 2.7.0, + // see https://git.io/fjfg5. + input.setSizeLimit(Integer.MAX_VALUE); return new FeedMessage(input, extensionRegistry); } }; diff --git a/src/main/java/org/opensphere/geometry/algorithm/ConcaveHull.java b/src/main/java/org/opensphere/geometry/algorithm/ConcaveHull.java index 43819f21311..0b2bdef3da6 100644 --- a/src/main/java/org/opensphere/geometry/algorithm/ConcaveHull.java +++ b/src/main/java/org/opensphere/geometry/algorithm/ConcaveHull.java @@ -13,22 +13,22 @@ import org.opensphere.geometry.triangulation.model.Triangle; import org.opensphere.geometry.triangulation.model.Vertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryCollection; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineSegment; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.Polygon; -import com.vividsolutions.jts.geom.impl.CoordinateArraySequence; -import com.vividsolutions.jts.operation.linemerge.LineMerger; -import com.vividsolutions.jts.triangulate.ConformingDelaunayTriangulationBuilder; -import com.vividsolutions.jts.triangulate.quadedge.QuadEdge; -import com.vividsolutions.jts.triangulate.quadedge.QuadEdgeSubdivision; -import com.vividsolutions.jts.triangulate.quadedge.QuadEdgeTriangle; -import com.vividsolutions.jts.util.UniqueCoordinateArrayFilter; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineSegment; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.impl.CoordinateArraySequence; +import org.locationtech.jts.operation.linemerge.LineMerger; +import org.locationtech.jts.triangulate.ConformingDelaunayTriangulationBuilder; +import org.locationtech.jts.triangulate.quadedge.QuadEdge; +import org.locationtech.jts.triangulate.quadedge.QuadEdgeSubdivision; +import org.locationtech.jts.triangulate.quadedge.QuadEdgeTriangle; +import org.locationtech.jts.util.UniqueCoordinateArrayFilter; /** * Computes a concave hull of a {@link Geometry} which is @@ -179,11 +179,11 @@ private Geometry concaveHull() { Collection quadEdges = qes.getEdges(); List qeTriangles = QuadEdgeTriangle.createOn(qes); - Collection qeVertices = + Collection qeVertices = qes.getVertices(false); int iV = 0; - for (com.vividsolutions.jts.triangulate.quadedge.Vertex v : qeVertices) { + for (org.locationtech.jts.triangulate.quadedge.Vertex v : qeVertices) { this.coordinates.put(v.getCoordinate(), iV); this.vertices.put(iV, new Vertex(iV, v.getCoordinate())); iV++; diff --git a/src/main/java/org/opensphere/geometry/triangulation/DoubleComparator.java b/src/main/java/org/opensphere/geometry/triangulation/DoubleComparator.java index 5aa1405b035..149312fa35c 100644 --- a/src/main/java/org/opensphere/geometry/triangulation/DoubleComparator.java +++ b/src/main/java/org/opensphere/geometry/triangulation/DoubleComparator.java @@ -4,7 +4,7 @@ import java.util.Comparator; import java.util.Map; -import com.vividsolutions.jts.triangulate.quadedge.QuadEdge; +import org.locationtech.jts.triangulate.quadedge.QuadEdge; /** * Comparator of a map containing QuadEdge as key diff --git a/src/main/java/org/opensphere/geometry/triangulation/model/Edge.java b/src/main/java/org/opensphere/geometry/triangulation/model/Edge.java index 8352673939d..4fd5035f6d5 100644 --- a/src/main/java/org/opensphere/geometry/triangulation/model/Edge.java +++ b/src/main/java/org/opensphere/geometry/triangulation/model/Edge.java @@ -4,7 +4,7 @@ import java.util.ArrayList; import java.util.List; -import com.vividsolutions.jts.geom.LineSegment; +import org.locationtech.jts.geom.LineSegment; /** * Edge. diff --git a/src/main/java/org/opensphere/geometry/triangulation/model/Vertex.java b/src/main/java/org/opensphere/geometry/triangulation/model/Vertex.java index 5018bfac078..32c2ea05e0f 100644 --- a/src/main/java/org/opensphere/geometry/triangulation/model/Vertex.java +++ b/src/main/java/org/opensphere/geometry/triangulation/model/Vertex.java @@ -1,7 +1,7 @@ /* This file is based on code copied from project OpenSphere, see the LICENSE file for further information. */ package org.opensphere.geometry.triangulation.model; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * Vertex. diff --git a/src/main/java/org/opentripplanner/analyst/PointFeature.java b/src/main/java/org/opentripplanner/analyst/PointFeature.java index 5df920eccbb..119d95637dd 100644 --- a/src/main/java/org/opentripplanner/analyst/PointFeature.java +++ b/src/main/java/org/opentripplanner/analyst/PointFeature.java @@ -10,10 +10,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; public class PointFeature implements Serializable { diff --git a/src/main/java/org/opentripplanner/analyst/PointSet.java b/src/main/java/org/opentripplanner/analyst/PointSet.java index 859b57d30fd..263d0d2d3c2 100644 --- a/src/main/java/org/opentripplanner/analyst/PointSet.java +++ b/src/main/java/org/opentripplanner/analyst/PointSet.java @@ -9,10 +9,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.MappingJsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Polygon; import gnu.trove.map.TObjectIntMap; import gnu.trove.map.hash.TObjectIntHashMap; import org.geojson.LngLatAlt; diff --git a/src/main/java/org/opentripplanner/analyst/RepeatedRaptorComparison.java b/src/main/java/org/opentripplanner/analyst/RepeatedRaptorComparison.java deleted file mode 100644 index 8edc1dc1277..00000000000 --- a/src/main/java/org/opentripplanner/analyst/RepeatedRaptorComparison.java +++ /dev/null @@ -1,269 +0,0 @@ -package org.opentripplanner.analyst; - -import com.vividsolutions.jts.geom.Coordinate; -import org.joda.time.LocalDate; -import org.mapdb.*; -import org.opentripplanner.analyst.cluster.ResultEnvelope; -import org.opentripplanner.analyst.cluster.TaskStatistics; -import org.opentripplanner.api.parameter.QualifiedModeSet; -import org.opentripplanner.common.MavenVersion; -import org.opentripplanner.graph_builder.GraphBuilder; -import org.opentripplanner.profile.ProfileRequest; -import org.opentripplanner.profile.RaptorWorker; -import org.opentripplanner.profile.RaptorWorkerData; -import org.opentripplanner.profile.RepeatedRaptorProfileRouter; -import org.opentripplanner.routing.core.TraverseModeSet; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.graph.Vertex; -import org.opentripplanner.standalone.CommandLineParameters; -import org.opentripplanner.common.Histogram; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -/** - * Compare results from Repeated RAPTOR runs. This is used to detect algorithmic changes that have - * had an effect on output. - * - * Usage: - * graph_directory [compare_file] [output_file] - * - * Graph directory is the directory from which to build the graph. Compare file is the previously saved - * file; if omitted it will not be used. Output file is the MapDB file in which - * to save output. If left blank it defaults to the OTP commit hash. - * - */ -public class RepeatedRaptorComparison { - private static final Logger LOG = LoggerFactory.getLogger(RepeatedRaptorComparison.class); - - // amounts to offset origins from vertices to test linking - public static final double OFFSET_X = 1e-4, OFFSET_Y = 1e-4; - - public static void main (String... args) { - if (args.length == 0) { - System.err.println("too few arguments."); - return; - } - - // build a graph - File graphDir = new File(args[0]); - Graph graph = buildGraph(graphDir); - - DB comparisonDb = null; - BTreeMap, Integer> comparison = null; - - // open the comparison file, if we have one. - if (args.length > 1) { - comparisonDb = DBMaker.newFileDB(new File(args[1])) - .readOnly() - .transactionDisable() - .closeOnJvmShutdown() - .cacheSize(24) - .asyncWriteEnable() - .make(); - - comparison = comparisonDb.getTreeMap("results"); - } - - String outputName = args.length > 2 ? args[2] : MavenVersion.VERSION.commit + ".db"; - - DB outputDb = DBMaker.newFileDB(new File(outputName)) - .transactionDisable() - .cacheSize(48) - .closeOnJvmShutdown() - .make(); - - final BTreeMap, Integer> output = outputDb.createTreeMap("results") - .valueSerializer(Serializer.JAVA) - .makeOrGet(); - - // if we have a comparison file, get the pointset from it. Otherwise choose some randomly. - Collection vertexLabels; - PointSet pset; - - if (comparison != null) { - // clooge, pointset is stored in its own map in db. - pset = comparisonDb.getTreeMap("pointset").get("pointset"); - } - else { - // choose some vertices - List vertices = graph.getVertices().stream() - // only use OSM nodes, because they are stable. e.g. splittervertices may not have stable identifiers between builds. - .filter(v -> v.getLabel().startsWith("osm:node:")) - .limit(1000) - .collect(Collectors.toList()); - - // make a pointset - pset = new PointSet(vertices.size()); - int featIdx = 0; - for (Vertex v : vertices) { - PointFeature pf = new PointFeature(); - pf.setId(v.getLabel()); - pf.setLat(v.getLat() + OFFSET_Y); - pf.setLon(v.getLon() + OFFSET_X); - pset.addFeature(pf, featIdx++); - } - - outputDb.createTreeMap("pointset") - .make().put("pointset", pset); - } - - SampleSet ss = new SampleSet(pset, graph.getSampleFactory()); - - final BTreeMap, Integer> comparisonResults = comparison; - - Histogram bestCaseHisto = new Histogram("Best case"); - Histogram avgCaseHisto = new Histogram("Average"); - Histogram worstCaseHisto = new Histogram("Worst case"); - - ProfileRequest template = new ProfileRequest(); - template.accessModes = new QualifiedModeSet("WALK"); - template.analyst = true; - template.maxWalkTime = 20 * 60; - template.walkSpeed = 1.3f; - template.fromTime = 7 * 3600; - template.toTime = 9 * 3600; - - template.date = new LocalDate(2015, 8, 4); - - RaptorWorkerData data = RepeatedRaptorProfileRouter.getRaptorWorkerData(template, graph, ss, new TaskStatistics()); - - // do the computation and comparison - IntStream.range(0, pset.featureCount()).parallel() - .forEach(idx -> { - - if (idx % 100 == 0) - System.out.println(idx + " points complete"); - - Coordinate coord = pset.getCoordinate(idx); - String origin = pset.getFeature(idx).getId(); - - ProfileRequest req; - try { - req = template.clone(); - } catch (CloneNotSupportedException e) { - /* can't happen */ - throw new RuntimeException(e); - } - - req.maxWalkTime = 20 * 60; - req.fromLat = req.toLat = coord.y; - req.fromLon = req.toLon = coord.x; - // 7 to 9 AM - req.fromTime = 7 * 3600; - req.toTime = 9 * 3600; - req.transitModes = new TraverseModeSet("TRANSIT"); - - RepeatedRaptorProfileRouter rrpr = new RepeatedRaptorProfileRouter(graph, req, ss); - rrpr.raptorWorkerData = data; - rrpr.includeTimes = true; - // TODO we really want to disable both isochrone and accessibility generation here. - // Because a sampleSet is provided it's going to make accessibility information (not isochrones). - - ResultEnvelope results = new ResultEnvelope(); - try { - results = rrpr.route(); - } catch (Exception e) { - LOG.error("Exception during routing", e); - return; - } - - for (ResultEnvelope.Which which : new ResultEnvelope.Which[] { - ResultEnvelope.Which.BEST_CASE, ResultEnvelope.Which.AVERAGE, - ResultEnvelope.Which.WORST_CASE }) { - Histogram histogram; - ResultSet resultSet; - - switch (which) { - case BEST_CASE: - histogram = bestCaseHisto; - resultSet = results.bestCase; - break; - case WORST_CASE: - histogram = worstCaseHisto; - resultSet = results.worstCase; - break; - case AVERAGE: - histogram = avgCaseHisto; - resultSet = results.avgCase; - break; - default: - histogram = null; - resultSet = null; - } - - // now that we have the proper histogram and result set, save them and do the - // comparison. - for (int i = 0; i < resultSet.times.length; i++) { - int time = resultSet.times[i]; - // TODO this is creating a PointFeature obj to hold the id at each call - // Cache? - String dest = pset.getFeature(i).getId(); - - Fun.Tuple3 key = new Fun.Tuple3<>( - origin, dest, which); - output.put(key, time); - - if (time < 0) { - LOG.error("Path from {} to {} has negative time {}", origin, dest, - time); - } - - if (comparisonResults != null) { - int time0 = comparisonResults.get(key); - - int deltaMinutes; - - if (time0 == RaptorWorker.UNREACHED && time != RaptorWorker.UNREACHED) - deltaMinutes = (time / 60) - 120; - else if (time == RaptorWorker.UNREACHED - && time0 != RaptorWorker.UNREACHED) - deltaMinutes = 120 - (time0 / 60); - else - deltaMinutes = (time - time0) / 60; - - // histograms are not threadsafe - synchronized (histogram) { - histogram.add(deltaMinutes); - } - } - } - } - }); - - output.close(); - if (comparisonDb != null) { - comparisonDb.close(); - - bestCaseHisto.displayHorizontal(); - System.out.println("mean: " + bestCaseHisto.mean()); - - avgCaseHisto.displayHorizontal(); - System.out.println("mean: " + avgCaseHisto.mean()); - - worstCaseHisto.displayHorizontal(); - System.out.println("mean: " + worstCaseHisto.mean()); - } - } - - private static Graph buildGraph(File directory) { - CommandLineParameters params = new CommandLineParameters(); - params.build = directory; - params.inMemory = true; - GraphBuilder graphBuilder = GraphBuilder.forDirectory(params, params.build); - graphBuilder.run(); - Graph graph = graphBuilder.getGraph(); - graph.routerId = "GRAPH"; - // re-index the graph to ensure all data is added and recreate a new streetIndex. It's ok to recreate the - // streetIndex because the previous one created during graph build is not needed anymore and isn't able to be - // used outside of the graphBuilder. - graph.index(true); - graph.index.clusterStopsAsNeeded(); - return graph; - } -} diff --git a/src/main/java/org/opentripplanner/analyst/TimeSurface.java b/src/main/java/org/opentripplanner/analyst/TimeSurface.java index 245c1f8a1ca..37a5a17d7a2 100644 --- a/src/main/java/org/opentripplanner/analyst/TimeSurface.java +++ b/src/main/java/org/opentripplanner/analyst/TimeSurface.java @@ -1,6 +1,6 @@ package org.opentripplanner.analyst; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import gnu.trove.iterator.TObjectIntIterator; import gnu.trove.map.TObjectIntMap; import gnu.trove.map.hash.TObjectIntHashMap; diff --git a/src/main/java/org/opentripplanner/analyst/batch/GraphGeographicFilter.java b/src/main/java/org/opentripplanner/analyst/batch/GraphGeographicFilter.java index babc0f5c8f3..bbd6e55eb89 100644 --- a/src/main/java/org/opentripplanner/analyst/batch/GraphGeographicFilter.java +++ b/src/main/java/org/opentripplanner/analyst/batch/GraphGeographicFilter.java @@ -13,11 +13,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.Point; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; public class GraphGeographicFilter implements IndividualFilter { diff --git a/src/main/java/org/opentripplanner/analyst/batch/ShapefilePopulation.java b/src/main/java/org/opentripplanner/analyst/batch/ShapefilePopulation.java index 9ff3d498dee..4778176a66c 100644 --- a/src/main/java/org/opentripplanner/analyst/batch/ShapefilePopulation.java +++ b/src/main/java/org/opentripplanner/analyst/batch/ShapefilePopulation.java @@ -14,10 +14,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; public class ShapefilePopulation extends BasicPopulation { diff --git a/src/main/java/org/opentripplanner/analyst/broker/Broker.java b/src/main/java/org/opentripplanner/analyst/broker/Broker.java deleted file mode 100644 index c5210453055..00000000000 --- a/src/main/java/org/opentripplanner/analyst/broker/Broker.java +++ /dev/null @@ -1,750 +0,0 @@ -package org.opentripplanner.analyst.broker; - -import com.amazonaws.regions.Regions; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.AmazonEC2Client; -import com.amazonaws.services.ec2.model.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.TreeMultimap; -import gnu.trove.map.TIntIntMap; -import gnu.trove.map.TIntObjectMap; -import gnu.trove.map.TObjectLongMap; -import gnu.trove.map.hash.TIntIntHashMap; -import gnu.trove.map.hash.TIntObjectHashMap; -import gnu.trove.map.hash.TObjectLongHashMap; -import org.glassfish.grizzly.http.server.Request; -import org.glassfish.grizzly.http.server.Response; -import org.glassfish.grizzly.http.util.HttpStatus; -import org.opentripplanner.analyst.cluster.AnalystClusterRequest; -import org.opentripplanner.analyst.cluster.AnalystWorker; -import org.opentripplanner.api.model.FeedScopedIdSerializer; -import org.opentripplanner.api.model.JodaLocalDateSerializer; -import org.opentripplanner.api.model.QualifiedModeSetSerializer; -import org.opentripplanner.api.model.TraverseModeSetSerializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; - -/** - * This class tracks incoming requests from workers to consume Analyst tasks, and attempts to match those - * requests to enqueued tasks. It aims to draw tasks fairly from all users, and fairly from all jobs within each user, - * while attempting to respect the graph affinity of each worker (give it tasks that require the same graph it has been - * working on recently). - * - * When no work is available or no workers are available, the polling functions return immediately, avoiding spin-wait. - * When they are receiving no work, workers are expected to disconnect and re-poll occasionally, on the order of 30 - * seconds. This serves as a signal to the broker that they are still alive and waiting. - * - * TODO if there is a backlog of work (the usual case when jobs are lined up) workers will constantly change graphs. - * Because (at least currently) two users never share the same graph, we can get by with pulling tasks cyclically or - * randomly from all the jobs, and just actively shaping the number of workers with affinity for each graph by forcing - * some of them to accept tasks on graphs other than the one they have declared affinity for. - * - * This could be thought of as "affinity homeostasis". We will constantly keep track of the ideal proportion of workers - * by graph (based on active queues), and the true proportion of consumers by graph (based on incoming requests) then - * we can decide when a worker's graph affinity should be ignored and what it should be forced to. - * - * It may also be helpful to mark jobs every time they are skipped in the LRU queue. Each time a job is serviced, - * it is taken out of the queue and put at its end. Jobs that have not been serviced float to the top. - */ -public class Broker implements Runnable { - - // TODO catalog of recently seen consumers by affinity with IP: response.getRequest().getRemoteAddr(); - - private static final Logger LOG = LoggerFactory.getLogger(Broker.class); - - /* How often we should check for delivered tasks that have timed out. */ - private static final int REDELIVERY_INTERVAL_SEC = 10; - - public final CircularList jobs = new CircularList<>(); - - /** the most tasks to deliver to a worker at a time */ - public final int MAX_TASKS_PER_WORKER = 8; - - /** - * How long to give workers to start up (in ms) before assuming that they have started (and starting more - * on a given graph if they haven't. - */ - public static final long WORKER_STARTUP_TIME = 60 * 60 * 1000; - - private int nUndeliveredTasks = 0; // Including normal priority jobs and high-priority tasks. - - private int nWaitingConsumers = 0; // including some that might be closed - - private int nextTaskId = 0; - - /** Maximum number of workers allowed */ - private int maxWorkers; - - private static final ObjectMapper mapper = new ObjectMapper(); - - private long nextRedeliveryCheckTime = System.currentTimeMillis(); - - static { - mapper.registerModule(FeedScopedIdSerializer.makeModule()); - mapper.registerModule(QualifiedModeSetSerializer.makeModule()); - mapper.registerModule(JodaLocalDateSerializer.makeModule()); - mapper.registerModule(TraverseModeSetSerializer.makeModule()); - } - - /** broker configuration */ - private final Properties config; - - /** configuration for workers launched by this broker */ - private final Properties workerConfig; - - private WorkerCatalog workerCatalog = new WorkerCatalog(); - - /** The time at which each task was delivered to a worker, to allow re-delivery. */ - TIntIntMap deliveryTimes = new TIntIntHashMap(); - - /** - * Requests that are not part of a job and can "cut in line" in front of jobs for immediate execution. - * When a high priority task is first received, we attempt to send it to a worker right away via - * the side channels. If that doesn't work, we put them here to be picked up the next time a worker - * is available via normal task distribution channels. - */ - private ArrayListMultimap stalledHighPriorityTasks = ArrayListMultimap.create(); - - /** - * High priority requests that have just come and are about to be sent down a single point channel. - * They put here for just 100 ms so that any that arrive together are batched to the same worker. - * If we didn't do this, two requests arriving at basically the same time could get fanned out to - * two different workers because the second came in in between closing the side channel and the worker - * reopening it. - */ - private Multimap newHighPriorityTasks = ArrayListMultimap.create(); - - /** Priority requests that have already been farmed out to workers, and are awaiting a response. */ - private TIntObjectMap highPriorityResponses = new TIntObjectHashMap<>(); - - /** Outstanding requests from workers for tasks, grouped by worker graph affinity. */ - Map> consumersByGraph = new HashMap<>(); - - /** - * Side channels used to send single point requests to workers, cutting in front of any other work on said workers. - * We use a TreeMultimap because it is ordered, and the wrapped response defines an order based on - * machine ID. This way, the same machine will tend to get all single point work for a graph, - * so multiple machines won't stay alive to do single point work. - */ - private Multimap singlePointChannels = TreeMultimap.create(); - - /** should we work offline */ - private boolean workOffline; - - private AmazonEC2 ec2; - - private Timer timer = new Timer(); - - private String workerName, project; - - /** - * keep track of which graphs we have launched workers on and how long ago we launched them, - * so that we don't re-request workers which have been requested. - */ - private TObjectLongMap recentlyRequestedWorkers = new TObjectLongHashMap<>(); - - // Queue of tasks to complete Delete, Enqueue etc. to avoid synchronizing all the functions ? - public Broker (Properties brokerConfig, String addr, int port) { - // print out date on startup so that CloudWatch logs has a unique fingerprint - LOG.info("Analyst worker starting at {}", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)); - - this.config = brokerConfig; - - // create a config for the AWS workers - workerConfig = new Properties(); - - // load the base worker configuration if specified - if (config.getProperty("worker-config") != null) { - try { - File f = new File(config.getProperty("worker-config")); - FileInputStream fis = new FileInputStream(f); - workerConfig.load(fis); - fis.close(); - } catch (IOException e) { - LOG.error("Error loading base worker configuration", e); - } - } - - workerConfig.setProperty("broker-address", addr); - workerConfig.setProperty("broker-port", "" + port); - - if (brokerConfig.getProperty("statistics-queue") != null) - workerConfig.setProperty("statistics-queue", brokerConfig.getProperty("statistics-queue")); - - workerConfig.setProperty("graphs-bucket", brokerConfig.getProperty("graphs-bucket")); - workerConfig.setProperty("pointsets-bucket", brokerConfig.getProperty("pointsets-bucket")); - - // Tell the workers to shut themselves down automatically - workerConfig.setProperty("auto-shutdown", "true"); - - Boolean workOffline = Boolean.parseBoolean(brokerConfig.getProperty("work-offline")); - if (workOffline == null) workOffline = true; - this.workOffline = workOffline; - - workerName = brokerConfig.getProperty("worker-name") != null ? brokerConfig.getProperty("worker-name") : "analyst-worker"; - project = brokerConfig.getProperty("project") != null ? brokerConfig.getProperty("project") : "analyst"; - - this.maxWorkers = brokerConfig.getProperty("max-workers") != null ? Integer.parseInt(brokerConfig.getProperty("max-workers")) : 4; - - ec2 = new AmazonEC2Client(); - - // default to current region when running in EC2 - com.amazonaws.regions.Region r = Regions.getCurrentRegion(); - - if (r != null) - ec2.setRegion(r); - } - - /** - * Enqueue a task for execution ASAP, planning to return the response over the same HTTP connection. - * Low-reliability, no re-delivery. - */ - public synchronized void enqueuePriorityTask (AnalystClusterRequest task, Response response) { - boolean workersAvailable = workersAvailableForGraph(task.graphId); - - if (!workersAvailable) { - createWorkersForGraph(task.graphId); - // chances are it won't be done in 30 seconds, but we want to poll frequently to avoid issues with phasing - try { - response.setHeader("Retry-After", "30"); - response.sendError(503, - "No workers available with this graph affinity, please retry shortly."); - } catch (IOException e) { - LOG.error("Could not finish high-priority 503 response", e); - } - } - - // if we're in offline mode, enqueue anyhow to kick the cluster to build the graph - // note that this will mean that requests get delivered multiple times in offline mode, - // so some unnecessary computation takes place - if (workersAvailable || workOffline) { - task.taskId = nextTaskId++; - newHighPriorityTasks.put(task.graphId, task); - highPriorityResponses.put(task.taskId, response); - - // wait 100ms to deliver to workers in case another request comes in almost simultaneously - timer.schedule(new TimerTask() { - @Override - public void run() { - deliverHighPriorityTasks(task.graphId); - } - }, 100); - } - - // do not notify task delivery thread just yet as we haven't put anything in the task delivery queue yet. - } - - /** attempt to deliver high priority tasks via side channels, or move them into normal channels if need be */ - public synchronized void deliverHighPriorityTasks (String graphId) { - Collection tasks = newHighPriorityTasks.get(graphId); - - if (tasks.isEmpty()) - // someone got here first - return; - - // try to deliver via side channels - Collection wrs = singlePointChannels.get(graphId); - - if (!wrs.isEmpty()) { - // there is (probably) a single point machine waiting to receive this - WrappedResponse wr = wrs.iterator().next(); - - try { - wr.response.setContentType("application/json"); - OutputStream os = wr.response.getOutputStream(); - mapper.writeValue(os, tasks); - os.close(); - wr.response.resume(); - - newHighPriorityTasks.removeAll(graphId); - - return; - } catch (Exception e) { - LOG.info("Failed to deliver single point job via side channel, reverting to normal channel", e); - } finally { - // remove responses whether they are dead or alive - removeSinglePointChannel(graphId, wr); - } - } - - // if we got here we didn't manage to send it via side channel, put it in the rotation for normal channels - // not using putAll as it retains a link to the original collection and then we get a concurrent modification exception later. - tasks.forEach(t -> stalledHighPriorityTasks.put(graphId, t)); - LOG.info("No side channel available for graph {}, delivering {} tasks via normal channel", - graphId, tasks.size()); - nUndeliveredTasks += tasks.size(); - newHighPriorityTasks.removeAll(graphId); - - // wake up delivery thread - notify(); - } - - /** Enqueue some tasks for queued execution possibly much later. Results will be saved to S3. */ - public synchronized void enqueueTasks (List tasks) { - Job job = findJob(tasks.get(0)); // creates one if it doesn't exist - - if (!workersAvailableForGraph(job.graphId)) - createWorkersForGraph(job.graphId); - - for (AnalystClusterRequest task : tasks) { - task.taskId = nextTaskId++; - job.addTask(task); - nUndeliveredTasks += 1; - LOG.debug("Enqueued task id {} in job {}", task.taskId, job.jobId); - if ( ! task.graphId.equals(job.graphId)) { - LOG.warn("Task graph ID {} does not match job graph ID {}.", task.graphId, job.graphId); - } - } - // Wake up the delivery thread if it's waiting on input. - // This wakes whatever thread called wait() while holding the monitor for this Broker object. - notify(); - } - - public boolean workersAvailableForGraph (String graphId) { - // make sure that we don't assign work to dead workers - workerCatalog.purgeDeadWorkers(); - - return !workerCatalog.workersByGraph.get(graphId).isEmpty(); - } - - /** Create workers for a given job, if need be */ - public void createWorkersForGraph (String graphId) { - String clientToken = UUID.randomUUID().toString().replaceAll("-", ""); - - if (workOffline) { - LOG.info("Work offline enabled, not creating workers for graph {}", graphId); - return; - } - - if (workerCatalog.observationsByWorkerId.size() >= maxWorkers) { - LOG.warn("{} workers already started, not starting more; jobs on graph {} will not complete", maxWorkers, graphId); - return; - } - - // don't re-request workers - if (recentlyRequestedWorkers.containsKey(graphId) - && recentlyRequestedWorkers.get(graphId) >= System.currentTimeMillis() - WORKER_STARTUP_TIME){ - LOG.info("workers still starting on graph {}, not starting more", graphId); - return; - } - - - // TODO: compute - int nWorkers = 1; - - LOG.info("Starting {} workers as there are none on graph {}", nWorkers, graphId); - // there are no workers on this graph, start one - RunInstancesRequest req = new RunInstancesRequest(); - req.setImageId(config.getProperty("ami-id")); - req.setInstanceType(InstanceType.valueOf(config.getProperty("worker-type"))); - req.setSubnetId(config.getProperty("subnet-id")); - - // even if we can't get all the workers we want at least get some - req.setMinCount(1); - req.setMaxCount(nWorkers); - - // it's fine to just modify the worker config as this method is synchronized - workerConfig.setProperty("initial-graph-id", graphId); - - ByteArrayOutputStream cfg = new ByteArrayOutputStream(); - try { - workerConfig.store(cfg, "Worker config"); - cfg.close(); - } catch (Exception e) { - throw new RuntimeException(e); - } - - // send the config as user data - String userData = new String(Base64.getEncoder().encode(cfg.toByteArray())); - req.setUserData(userData); - - if (config.getProperty("worker-iam-role") != null) - req.setIamInstanceProfile(new IamInstanceProfileSpecification().withArn(config.getProperty("worker-iam-role"))); - - // launch into a VPC if desired - if (config.getProperty("subnet") != null) - req.setSubnetId(config.getProperty("subnet")); - - // allow us to retry request at will - req.setClientToken(clientToken); - // allow machine to shut itself completely off - req.setInstanceInitiatedShutdownBehavior(ShutdownBehavior.Terminate); - RunInstancesResult res = ec2.runInstances(req); - res.getReservation().getInstances().forEach(i -> { - Collection tags = Arrays.asList( - new Tag("name", workerName), - new Tag("project", project) - ); - i.setTags(tags); - }); - recentlyRequestedWorkers.put(graphId, System.currentTimeMillis()); - LOG.info("Requesting {} workers", nWorkers); - } - - /** Consumer long-poll operations are enqueued here. */ - public synchronized void registerSuspendedResponse(String graphId, Response response) { - // Add this worker to our catalog, tracking its graph affinity and the last time it was seen. - String workerId = response.getRequest().getHeader(AnalystWorker.WORKER_ID_HEADER); - if (workerId != null && !workerId.isEmpty()) { - workerCatalog.catalog(workerId, graphId); - } else { - LOG.error("Worker did not supply a unique ID for itself . Ignoring it."); - return; - } - // Shelf this suspended response in a queue grouped by graph affinity. - Deque deque = consumersByGraph.get(graphId); - if (deque == null) { - deque = new ArrayDeque<>(); - consumersByGraph.put(graphId, deque); - } - deque.addLast(response); - nWaitingConsumers += 1; - // Wake up the delivery thread if it's waiting on consumers. - // This is whatever thread called wait() while holding the monitor for this Broker object. - notify(); - } - - /** When we notice that a long poll connection has closed, we remove it here. */ - public synchronized boolean removeSuspendedResponse(String graphId, Response response) { - Deque deque = consumersByGraph.get(graphId); - if (deque == null) { - return false; - } - if (deque.remove(response)) { - nWaitingConsumers -= 1; - LOG.debug("Removed closed connection from queue."); - logQueueStatus(); - return true; - } - return false; - } - - /** - * Register an HTTP connection that can be used to send single point requests directly to - * workers, bypassing normal task distribution channels. - */ - public synchronized void registerSinglePointChannel (String graphAffinity,WrappedResponse response) { - singlePointChannels.put(graphAffinity, response); - // no need to notify as the side channels are not used by the normal task delivery loop - } - - /** - * Remove a single point channel because the connection was closed. - */ - public synchronized boolean removeSinglePointChannel (String graphAffinity, WrappedResponse response) { - return singlePointChannels.remove(graphAffinity, response); - } - - private void logQueueStatus() { - LOG.info("{} undelivered, of which {} high-priority", nUndeliveredTasks, - stalledHighPriorityTasks.size()); - LOG.info("{} producers waiting, {} consumers waiting", highPriorityResponses.size(), nWaitingConsumers); - LOG.info("{} total workers", workerCatalog.size()); - } - - /** - * Check whether there are any delivered tasks that have reached their invisibility timeout but have not yet been - * marked complete. Enqueue those tasks for redelivery. - */ - private void redeliver() { - if (System.currentTimeMillis() > nextRedeliveryCheckTime) { - nextRedeliveryCheckTime += REDELIVERY_INTERVAL_SEC * 1000; - LOG.info("Scanning for redelivery..."); - int nRedelivered = 0; - int nInvisible = 0; - for (Job job : jobs) { - nInvisible += job.invisibleUntil.size(); - nRedelivered += job.redeliver(); - } - LOG.info("{} tasks enqueued for redelivery out of {} invisible tasks.", nRedelivered, nInvisible); - nUndeliveredTasks += nRedelivered; - } - } - - /** - * This method checks whether there are any high-priority tasks or normal job tasks and attempts to match them with - * waiting workers. It blocks until there are tasks or workers available. - */ - public synchronized void deliverTasks() throws InterruptedException { - - // Wait until there are some undelivered tasks. - while (nUndeliveredTasks == 0) { - LOG.debug("Task delivery thread is going to sleep, there are no tasks waiting for delivery."); - logQueueStatus(); - wait(); - redeliver(); - } - LOG.debug("Task delivery thread is awake and there are some undelivered tasks."); - logQueueStatus(); - - while (nWaitingConsumers == 0) { - LOG.debug("Task delivery thread is going to sleep, there are no consumers waiting."); - // Thread will be notified when there are new incoming consumer connections. - wait(); - } - - LOG.debug("Task delivery thread awake; consumers are waiting and tasks are available"); - - // Loop over all jobs and send them to consumers - // This makes for an as-fair-as-possible allocation: jobs are fairly allocated between - // workers on their graph. - - // start with high-priority tasks - HIGHPRIORITY: for (Map.Entry> e : stalledHighPriorityTasks - .asMap().entrySet()) { - // the collection is an arraylist with the most recently added at the end - String graphId = e.getKey(); - Collection tasks = e.getValue(); - - // see if there are any consumers for this - // don't respect graph affinity when working offline; we can't arbitrarily start more workers - Deque consumers; - if (!workOffline) - consumers = consumersByGraph.get(graphId); - else { - Optional> opt = consumersByGraph.values().stream().filter(c -> !c.isEmpty()).findFirst(); - if (opt.isPresent()) consumers = opt.get(); - else consumers = null; - } - - if (consumers == null || consumers.isEmpty()) { - LOG.warn("No consumer found for graph {}, needed for {} high-priority tasks", graphId, tasks.size()); - continue HIGHPRIORITY; - } - - Iterator taskIt = tasks.iterator(); - while (taskIt.hasNext() && !consumers.isEmpty()) { - Response consumer = consumers.pop(); - - // package tasks into a job - Job job = new Job("HIGH PRIORITY"); - job.graphId = graphId; - for (int i = 0; i < MAX_TASKS_PER_WORKER && taskIt.hasNext(); i++) { - job.addTask(taskIt.next()); - taskIt.remove(); - } - - // TODO inefficiency here: we should mix single point and multipoint in the same response - deliver(job, consumer); - nWaitingConsumers--; - } - } - - // deliver low priority tasks - while (nWaitingConsumers > 0) { - // ensure we advance at least one; advanceToElement will not advance if the predicate passes - // for the first element. - jobs.advance(); - - // find a job that both has visible tasks and has available workers - // We don't respect graph affinity when working offline, because we can't start more workers - Job current; - if (!workOffline) { - current = jobs.advanceToElement(e -> !e.tasksAwaitingDelivery.isEmpty() && - consumersByGraph.containsKey(e.graphId) && - !consumersByGraph.get(e.graphId).isEmpty()); - } - else { - current = jobs.advanceToElement(e -> !e.tasksAwaitingDelivery.isEmpty()); - } - - // nothing to see here - if (current == null) break; - - Deque consumers; - if (!workOffline) - consumers = consumersByGraph.get(current.graphId); - else { - Optional> opt = consumersByGraph.values().stream().filter(c -> !c.isEmpty()).findFirst(); - if (opt.isPresent()) consumers = opt.get(); - else consumers = null; - } - // deliver this job to only one consumer - // This way if there are multiple workers and multiple jobs the jobs will be fairly distributed, more or less - deliver(current, consumers.pop()); - nWaitingConsumers--; - } - - // TODO: graph switching - - // we've delivered everything we can, prevent anything else from happening until something changes - wait(); - } - - /** - * This uses a linear search through jobs, which should not be problematic unless there are thousands of - * simultaneous jobs. - * @return a Job object that contains the given task ID. - */ - public Job getJobForTask (int taskId) { - for (Job job : jobs) { - if (job.containsTask(taskId)) { - return job; - } - } - return null; - } - - /** - * Attempt to hand some tasks from the given job to a waiting consumer connection. - * The write will fail if the consumer has closed the connection but it hasn't been removed from the connection - * queue yet. This can happen because the Broker methods are synchronized, and the removal action may be waiting - * to get the monitor while we are trying to distribute tasks here. - * @return whether the handoff succeeded. - */ - public synchronized boolean deliver (Job job, Response response) { - - // Check up-front whether the connection is still open. - if (!response.getRequest().getRequest().getConnection().isOpen()) { - LOG.debug("Consumer connection was closed. It will be removed."); - return false; - } - - // Get up to N tasks from the tasksAwaitingDelivery deque - List tasks = new ArrayList<>(); - while (tasks.size() < MAX_TASKS_PER_WORKER && !job.tasksAwaitingDelivery.isEmpty()) { - tasks.add(job.tasksAwaitingDelivery.poll()); - } - - // Attempt to deliver the tasks to the given consumer. - try { - response.setStatus(HttpStatus.OK_200); - OutputStream out = response.getOutputStream(); - mapper.writeValue(out, tasks); - response.resume(); - } catch (IOException e) { - // The connection was probably closed by the consumer, but treat it as a server error. - LOG.debug("Consumer connection caused IO error, it will be removed."); - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); - response.resume(); - // Delivery failed, put tasks back on (the end of) the queue. - job.tasksAwaitingDelivery.addAll(tasks); - return false; - } - - // Delivery succeeded, move tasks from undelivered to delivered status - LOG.debug("Delivery of {} tasks succeeded.", tasks.size()); - nUndeliveredTasks -= tasks.size(); - job.markTasksDelivered(tasks); - - return true; - - } - - /** - * Take a normal (non-priority) task out of a job queue, marking it as completed so it will not be re-delivered. - * TODO maybe use unique delivery receipts instead of task IDs to handle redelivered tasks independently - * @return whether the task was found and removed. - */ - public synchronized boolean markTaskCompleted (int taskId) { - Job job = getJobForTask(taskId); - if (job == null) { - LOG.error("Could not find a job containing task {}, and therefore could not mark the task as completed."); - return false; - } - job.markTaskCompleted(taskId); - return true; - } - - /** - * Marks the specified priority request as completed, and returns the suspended Response object for the connection - * that submitted the priority request (the UI), which probably still waiting to receive a result back over the - * same connection. A HttpHandler thread can then pump data from the DELETE body back to the origin of the request, - * without blocking the broker thread. - * TODO rename to "deregisterSuspendedProducer" and "deregisterSuspendedConsumer" ? - */ - public synchronized Response deletePriorityTask (int taskId) { - return highPriorityResponses.remove(taskId); - } - - // TODO: occasionally purge closed connections from consumersByGraph - // TODO: worker catalog and graph affinity homeostasis - - @Override - public void run() { - while (true) { - try { - deliverTasks(); - } catch (InterruptedException e) { - LOG.info("Task pump thread was interrupted."); - return; - } - } - } - - /** find the job for a task, creating it if it does not exist */ - public Job findJob (AnalystClusterRequest task) { - Job job = findJob(task.jobId); - - if (job != null) - return job; - - job = new Job(task.jobId); - job.graphId = task.graphId; - jobs.insertAtTail(job); - return job; - } - - /** find the job for a jobId, or null if it does not exist */ - public Job findJob (String jobId) { - for (Job job : jobs) { - if (job.jobId.equals(jobId)) { - return job; - } - } - return null; - } - - /** delete a job */ - public synchronized boolean deleteJob (String jobId) { - Job job = findJob(jobId); - if (job == null) return false; - nUndeliveredTasks -= job.tasksAwaitingDelivery.size(); - return jobs.remove(job); - } - - private Multimap activeJobsPerGraph = HashMultimap.create(); - - public synchronized boolean anyJobsActive() { - for (Job job : jobs) { - if (!job.isComplete()) return true; - } - return false; - } - - void activateJob (Job job) { - activeJobsPerGraph.put(job.graphId, job.jobId); - } - - void deactivateJob (Job job) { - activeJobsPerGraph.remove(job.graphId, job.jobId); - } - - /** - * We wrap responses in a class that has a machine ID, and then put them in a TreeSet so that - * the machine with the lowest ID on a given graph always gets single-point work. The reason - * for this is so that a single machine will tend to get single-point work and thus we don't - * unnecessarily keep multiple multipoint machines alive. - */ - public static class WrappedResponse implements Comparable { - public final Response response; - public final String machineId; - - public WrappedResponse(Request request, Response response) { - this.response = response; - this.machineId = request.getHeader(AnalystWorker.WORKER_ID_HEADER); - } - - @Override public int compareTo(WrappedResponse wrappedResponse) { - return this.machineId.compareTo(wrappedResponse.machineId); - } - } -} diff --git a/src/main/java/org/opentripplanner/analyst/broker/BrokerHttpHandler.java b/src/main/java/org/opentripplanner/analyst/broker/BrokerHttpHandler.java deleted file mode 100644 index 4524c0b592c..00000000000 --- a/src/main/java/org/opentripplanner/analyst/broker/BrokerHttpHandler.java +++ /dev/null @@ -1,229 +0,0 @@ -package org.opentripplanner.analyst.broker; - -import com.conveyal.geojson.GeoJsonModule; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.io.ByteStreams; -import org.glassfish.grizzly.http.Method; -import org.glassfish.grizzly.http.server.HttpHandler; -import org.glassfish.grizzly.http.server.Request; -import org.glassfish.grizzly.http.server.Response; -import org.glassfish.grizzly.http.util.HttpStatus; -import org.opentripplanner.analyst.cluster.AnalystClusterRequest; -import org.opentripplanner.api.model.FeedScopedIdSerializer; -import org.opentripplanner.api.model.JodaLocalDateSerializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -/** -* A Grizzly Async Http Service (uses reponse suspend/resume) - * https://blogs.oracle.com/oleksiys/entry/grizzly_2_0_httpserver_api1 - * - * When resuming a response object, "The only reliable way to check the socket status is to try to read or - * write something." Though you also have: - * - * response.getRequest().getRequest().getConnection().isOpen() - * response.getRequest().getRequest().getConnection().addCloseListener(); - * But none of these work, I've tried all three of them. You can even write to the outputstream after the connection - * is closed. - * Solution: networkListener.getTransport().setIOStrategy(SameThreadIOStrategy.getInstance()); - * This makes all three work! isOpen, CloseListener, and IOExceptions from flush(); - * - * Grizzly has Comet support, but this seems geared toward subscriptions to broadcast events. - * -*/ -class BrokerHttpHandler extends HttpHandler { - private static final Logger LOG = LoggerFactory.getLogger(BrokerHttpHandler.class); - - // TODO we should really just make one static mapper somewhere and use it throughout OTP - private ObjectMapper mapper = new ObjectMapper() - .registerModule(FeedScopedIdSerializer.makeModule()) - .registerModule(JodaLocalDateSerializer.makeModule()) - .registerModule(new GeoJsonModule()) - .setSerializationInclusion(JsonInclude.Include.NON_NULL);; - - private Broker broker; - - public BrokerHttpHandler(Broker broker) { - this.broker = broker; - } - - @Override - public void service(Request request, Response response) throws Exception { - - response.setContentType("application/json"); - - // request.getRequestURI(); // without protocol or server, only request path - // request.getPathInfo(); // without handler base path - String[] pathComponents = request.getPathInfo().split("/"); - // Path component 0 is empty since the path always starts with a slash. - if (pathComponents.length < 2) { - response.setStatus(HttpStatus.BAD_REQUEST_400); - response.setDetailMessage("path should have at least one part"); - } - - try { - if (request.getMethod() == Method.HEAD) { - /* Let the client know server is alive and URI + request are valid. */ - mapper.readTree(request.getInputStream()); - response.setStatus(HttpStatus.OK_200); - return; - } else if (request.getMethod() == Method.GET && "status".equals(pathComponents[1])) { - /* fetch job status */ - String[] jobIds = pathComponents[2].split(","); - - List ret = Arrays.asList(jobIds).stream() - .map(id -> broker.findJob(id)) - .filter(job -> job != null) - .map(job -> new JobStatus(job)) - .collect(Collectors.toList()); - - if (ret.isEmpty()) { - response.setStatus(HttpStatus.NOT_FOUND_404); - response.setDetailMessage("no job IDs were found"); - } - else { - response.setStatus(HttpStatus.OK_200); - OutputStream os = response.getOutputStream(); - mapper.writeValue(os, ret); - os.close(); - } - return; - } else if (request.getMethod() == Method.POST) { - /* dequeue messages. */ - String command = pathComponents[1]; - - if ("dequeue".equals(command)) { - String graphAffinity = pathComponents[2]; - request.getRequest().getConnection() - .addCloseListener((closeable, iCloseType) -> { - broker.removeSuspendedResponse(graphAffinity, response); - }); - response.suspend(); // The request should survive after the handler function exits. - broker.registerSuspendedResponse(graphAffinity, response); - } - - /* not dequeueing, enqueuing */ - else if ("enqueue".equals(command)) { - String context = pathComponents[2]; - if ("priority".equals(context)) { - // Enqueue a single priority task - AnalystClusterRequest task = mapper.readValue(request.getInputStream(), - AnalystClusterRequest.class); - broker.enqueuePriorityTask(task, response); - // Enqueueing the priority task has set its internal taskId. - // TODO move all removal listener registration into the broker functions. - request.getRequest().getConnection() - .addCloseListener((closeable, iCloseType) -> { - broker.deletePriorityTask(task.taskId); - }); - response.suspend(); // The request should survive after the handler function exits. - return; - - } else if ("jobs".equals(context)) { - // Enqueue a list of tasks that belong to jobs - List tasks = mapper - .readValue(request.getInputStream(), - new TypeReference>() { - }); - // Pre-validate tasks checking that they are all on the same job - AnalystClusterRequest exemplar = tasks.get(0); - for (AnalystClusterRequest task : tasks) { - if (task.jobId != exemplar.jobId || task.graphId != exemplar.graphId) { - response.setStatus(HttpStatus.BAD_REQUEST_400); - response.setDetailMessage( - "All tasks must be for the same graph and job."); - } - } - broker.enqueueTasks(tasks); - response.setStatus(HttpStatus.ACCEPTED_202); - } else { - response.setStatus(HttpStatus.NOT_FOUND_404); - response.setDetailMessage( - "Context not found; should be either 'jobs' or 'priority'"); - } - } - else if ("complete".equals(command)) { - // Mark a specific high-priority task as completed, and record its result. - // We were originally planning to do this with a DELETE request that has a body, - // but that is nonstandard enough to anger many libraries including Grizzly. - int taskId = Integer.parseInt(pathComponents[3]); - Response suspendedProducerResponse = broker.deletePriorityTask(taskId); - if (suspendedProducerResponse == null) { - response.setStatus(HttpStatus.NOT_FOUND_404); - return; - } - // Copy the result back to the connection that was the source of the task. - try { - ByteStreams.copy(request.getInputStream(), - suspendedProducerResponse.getOutputStream()); - } catch (IOException ioex) { - // Apparently the task producer did not wait to retrieve its result. Priority task result delivery - // is not guaranteed, we don't need to retry, this is not considered an error by the worker. - } - response.setStatus(HttpStatus.OK_200); - suspendedProducerResponse.setStatus(HttpStatus.OK_200); - suspendedProducerResponse.resume(); - return; - } - else if ("single".equals(command)) { - // await single point responses - String graphAffinity = pathComponents[2]; - Broker.WrappedResponse wr = new Broker.WrappedResponse(request, response); - request.getRequest().getConnection().addCloseListener((c, i) -> { - broker.removeSinglePointChannel(graphAffinity, wr); - }); - response.suspend(); - broker.registerSinglePointChannel(graphAffinity, wr); - } - } else if (request.getMethod() == Method.DELETE) { - /* Acknowledge completion of a task and remove it from queues, avoiding re-delivery. */ - if ("tasks".equalsIgnoreCase(pathComponents[1])) { - int taskId = Integer.parseInt(pathComponents[2]); - // This must not have been a priority task. Try to delete it as a normal job task. - if (broker.markTaskCompleted(taskId)) { - response.setStatus(HttpStatus.OK_200); - } else { - response.setStatus(HttpStatus.NOT_FOUND_404); - } - } else if ("jobs".equals((pathComponents[1]))) { - if (broker.deleteJob(pathComponents[2])) { - response.setStatus(HttpStatus.OK_200); - response.setDetailMessage("job deleted"); - } - else { - response.setStatus(HttpStatus.NOT_FOUND_404); - response.setDetailMessage("job not found"); - } - } else { - response.setStatus(HttpStatus.BAD_REQUEST_400); - response.setDetailMessage("Delete is only allowed for tasks and jobs."); - } - } else { - response.setStatus(HttpStatus.BAD_REQUEST_400); - response.setDetailMessage("Unrecognized HTTP method."); - } - } catch (JsonProcessingException jpex) { - response.setStatus(HttpStatus.BAD_REQUEST_400); - response.setDetailMessage("Could not decode/encode JSON payload. " + jpex.getMessage()); - LOG.info("Error processing JSON from client", jpex); - } catch (Exception ex) { - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); - response.setDetailMessage(ex.toString()); - LOG.info("Error processing client request", ex); - } - } - - public void writeJson (Response response, Object object) throws IOException { - mapper.writeValue(response.getOutputStream(), object); - } - -} diff --git a/src/main/java/org/opentripplanner/analyst/broker/BrokerMain.java b/src/main/java/org/opentripplanner/analyst/broker/BrokerMain.java deleted file mode 100644 index 255ffc504a4..00000000000 --- a/src/main/java/org/opentripplanner/analyst/broker/BrokerMain.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.opentripplanner.analyst.broker; - -import org.glassfish.grizzly.http.server.HttpServer; -import org.glassfish.grizzly.http.server.NetworkListener; -import org.glassfish.grizzly.strategies.SameThreadIOStrategy; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.BindException; -import java.util.Properties; - -// benchmark: $ ab -n 2000 -k -c 100 http://localhost:9001/ - -// TODO Merge with Broker -public class BrokerMain implements Runnable { - - private static final Logger LOG = LoggerFactory.getLogger(BrokerMain.class); - - private static final int DEFAULT_PORT = 9001; - - private static final String DEFAULT_BIND_ADDRESS = "0.0.0.0"; - - Properties config = new Properties(); - - public Broker broker; - - public static void main(String[] args) { - - File cfg; - if (args.length > 0) - cfg = new File(args[0]); - else - cfg = new File("broker.conf"); - - if (!cfg.exists()) { - LOG.error("Broker configuration file {} not found", cfg); - return; - } - - Properties brokerConfig = new Properties(); - try { - FileInputStream is = new FileInputStream(cfg); - brokerConfig.load(is); - is.close(); - } catch (IOException e) { - LOG.error("Error reading config file {}", e); - return; - } - - // Create instance and run in the current thread. - new BrokerMain(brokerConfig).run(); - - } - - public BrokerMain(Properties brokerConfig) { - this.config = brokerConfig; - } - - public void run() { - int port = config.getProperty("port") != null ? Integer.parseInt(config.getProperty("port")) : DEFAULT_PORT; - String addr = config.getProperty("bind-address") != null ? config.getProperty("bind-address") : DEFAULT_BIND_ADDRESS; - LOG.info("Starting analyst broker on port {} of interface {}", port, addr); - HttpServer httpServer = new HttpServer(); - NetworkListener networkListener = new NetworkListener("broker", addr, port); - // We avoid blocking IO, and the following line allows us to see closed connections. - networkListener.getTransport().setIOStrategy(SameThreadIOStrategy.getInstance()); - httpServer.addListener(networkListener); - // Bypass Jersey etc. and add a low-level Grizzly handler. - // As in servlets, * is needed in base path to identify the "rest" of the path. - broker = new Broker(config, addr, port); - httpServer.getServerConfiguration().addHttpHandler(new BrokerHttpHandler(broker), "/*"); - try { - httpServer.start(); - LOG.info("Broker running."); - broker.run(); // run queue broker task pump in this thread - Thread.currentThread().join(); - } catch (BindException be) { - LOG.error("Cannot bind to port {}. Is it already in use?", port); - } catch (IOException ioe) { - LOG.error("IO exception while starting server."); - } catch (InterruptedException ie) { - LOG.info("Interrupted, shutting down."); - } - httpServer.shutdown(); - } - -} diff --git a/src/main/java/org/opentripplanner/analyst/cluster/AnalystWorker.java b/src/main/java/org/opentripplanner/analyst/cluster/AnalystWorker.java deleted file mode 100644 index 6cdddde56ba..00000000000 --- a/src/main/java/org/opentripplanner/analyst/cluster/AnalystWorker.java +++ /dev/null @@ -1,661 +0,0 @@ -package org.opentripplanner.analyst.cluster; - -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.conveyal.geojson.GeoJsonModule; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.config.SocketConfig; -import org.apache.http.conn.HttpHostConnectException; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.message.BasicHeader; -import org.apache.http.util.EntityUtils; -import org.opentripplanner.analyst.PointSet; -import org.opentripplanner.analyst.SampleSet; -import org.opentripplanner.api.model.FeedScopedIdSerializer; -import org.opentripplanner.api.model.JodaLocalDateSerializer; -import org.opentripplanner.api.model.QualifiedModeSetSerializer; -import org.opentripplanner.api.model.TraverseModeSetSerializer; -import org.opentripplanner.common.MavenVersion; -import org.opentripplanner.profile.RaptorWorkerData; -import org.opentripplanner.profile.RepeatedRaptorProfileRouter; -import org.opentripplanner.routing.graph.Graph; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.net.SocketTimeoutException; -import java.net.URI; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Properties; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPOutputStream; - -/** - * - */ -public class AnalystWorker implements Runnable { - - /** - * worker ID - just a random ID so we can differentiate machines used for computation. - * Useful to isolate the logs from a particular machine, as well as to evaluate any - * variation in performance coming from variation in the performance of the underlying - * VMs. - * - * This needs to be static so the logger can access it; see the static member class - * WorkerIdDefiner. A side effect is that only one worker can run in a given JVM. It also - * needs to be defined before the logger is defined, so that it is initialized before the - * logger is. - */ - public static final String machineId = UUID.randomUUID().toString().replaceAll("-", ""); - - private static final Logger LOG = LoggerFactory.getLogger(AnalystWorker.class); - - public static final String WORKER_ID_HEADER = "X-Worker-Id"; - - public static final int POLL_TIMEOUT = 10 * 1000; - - /** - * If this value is non-negative, the worker will not actually do any work. It will just report all tasks - * as completed immediately, but will fail to do so on the given percentage of tasks. This is used in testing task - * re-delivery and overall broker sanity. - */ - public int dryRunFailureRate = -1; - - /** How long (minimum, in milliseconds) should this worker stay alive after receiving a single point request? */ - public static final int SINGLE_POINT_KEEPALIVE = 15 * 60 * 1000; - - /** should this worker shut down automatically */ - public final boolean autoShutdown; - - public static final Random random = new Random(); - - private TaskStatisticsStore statsStore; - - /** is there currently a channel open to the broker to receive single point jobs? */ - private volatile boolean sideChannelOpen = false; - - ObjectMapper objectMapper; - - String BROKER_BASE_URL = "http://localhost:9001"; - - static final HttpClient httpClient; - - /** Cache RAPTOR data by Job ID */ - private Cache workerDataCache = CacheBuilder.newBuilder() - .maximumSize(200) - .build(); - - static { - PoolingHttpClientConnectionManager mgr = new PoolingHttpClientConnectionManager(); - mgr.setDefaultMaxPerRoute(20); - - int timeout = 10 * 1000; - SocketConfig cfg = SocketConfig.custom() - .setSoTimeout(timeout) - .build(); - mgr.setDefaultSocketConfig(cfg); - - httpClient = HttpClients.custom() - .setConnectionManager(mgr) - .build(); - } - - // Of course this will eventually need to be shared between multiple AnalystWorker threads. - ClusterGraphBuilder clusterGraphBuilder; - - // Of course this will eventually need to be shared between multiple AnalystWorker threads. - PointSetDatastore pointSetDatastore; - - // Clients for communicating with Amazon web services - AmazonS3 s3; - - String graphId = null; - long startupTime, nextShutdownCheckTime; - - // Region awsRegion = Region.getRegion(Regions.EU_CENTRAL_1); - Region awsRegion = Region.getRegion(Regions.US_EAST_1); - - /** aws instance type, or null if not running on AWS */ - private String instanceType; - - long lastHighPriorityRequestProcessed = 0; - - /** - * Queue for high-priority tasks. Should be plenty long enough to hold all that have come in - - * we don't need to block on polling the manager. - */ - private ThreadPoolExecutor highPriorityExecutor, batchExecutor; - - public AnalystWorker(Properties config) { - // print out date on startup so that CloudWatch logs has a unique fingerprint - LOG.info("Analyst worker starting at {}", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)); - - // parse the configuration - // set up the stats store - String statsQueue = config.getProperty("statistics-queue"); - if (statsQueue != null) - this.statsStore = new SQSTaskStatisticsStore(statsQueue); - else - // a stats store that does nothing. - this.statsStore = s -> {}; - - String addr = config.getProperty("broker-address"); - String port = config.getProperty("broker-port"); - - if (addr != null) { - if (port != null) - this.BROKER_BASE_URL = String.format("http://%s:%s", addr, port); - else - this.BROKER_BASE_URL = String.format("http://%s", addr); - } - - // set the initial graph affinity of this worker (if it is not in the config file it will be - // set to null, i.e. no graph affinity) - // we don't actually build the graph now; this is just a hint to the broker as to what - // graph this machine was intended to analyze. - this.graphId = config.getProperty("initial-graph-id"); - - this.pointSetDatastore = new PointSetDatastore(10, null, false, config.getProperty("pointsets-bucket")); - this.clusterGraphBuilder = new ClusterGraphBuilder(config.getProperty("graphs-bucket")); - - Boolean autoShutdown = Boolean.parseBoolean(config.getProperty("auto-shutdown")); - this.autoShutdown = autoShutdown == null ? false : autoShutdown; - - // Consider shutting this worker down once per hour, starting 55 minutes after it started up. - startupTime = System.currentTimeMillis(); - nextShutdownCheckTime = startupTime + 55 * 60 * 1000; - - // When creating the S3 and SQS clients use the default credentials chain. - // This will check environment variables and ~/.aws/credentials first, then fall back on - // the auto-assigned IAM role if this code is running on an EC2 instance. - // http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-roles.html - s3 = new AmazonS3Client(); - s3.setRegion(awsRegion); - - /* The ObjectMapper (de)serializes JSON. */ - objectMapper = new ObjectMapper(); - objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true); - objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // ignore JSON fields that don't match target type - - /* Tell Jackson how to (de)serialize AgencyAndIds, which appear as map keys in routing requests. */ - objectMapper.registerModule(FeedScopedIdSerializer.makeModule()); - - /* serialize/deserialize qualified mode sets */ - objectMapper.registerModule(QualifiedModeSetSerializer.makeModule()); - - /* serialize/deserialize Joda dates */ - objectMapper.registerModule(JodaLocalDateSerializer.makeModule()); - - /* serialize/deserialize traversemodesets */ - objectMapper.registerModule(TraverseModeSetSerializer.makeModule()); - - objectMapper.registerModule(new GeoJsonModule()); - - instanceType = getInstanceType(); - } - - /** - * This is the main worker event loop which fetches tasks from a broker and schedules them for execution. - * It maintains a small local queue on the worker so that it doesn't idle while fetching new tasks. - */ - @Override - public void run() { - // create executors with up to one thread per processor - int nP = Runtime.getRuntime().availableProcessors(); - highPriorityExecutor = new ThreadPoolExecutor(1, nP, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(255)); - highPriorityExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); - batchExecutor = new ThreadPoolExecutor(1, nP, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(nP * 2)); - batchExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); - - // Build a graph on startup, iff a graph ID was provided. - if (graphId != null) { - LOG.info("Prebuilding graph {}", graphId); - Graph graph = clusterGraphBuilder.getGraph(graphId); - // also prebuild the stop tree cache - graph.index.getStopTreeCache(); - LOG.info("Done prebuilding graph {}", graphId); - } - - // Start filling the work queues. - boolean idle = false; - while (true) { - long now = System.currentTimeMillis(); - // Consider shutting down if enough time has passed - if (now > nextShutdownCheckTime && autoShutdown) { - if (idle && now > lastHighPriorityRequestProcessed + SINGLE_POINT_KEEPALIVE) { - LOG.warn("Machine is idle, shutting down."); - try { - Process process = new ProcessBuilder("sudo", "/sbin/shutdown", "-h", "now") - .start(); - process.waitFor(); - } catch (Exception ex) { - LOG.error("Unable to terminate worker", ex); - } finally { - System.exit(0); - } - } - nextShutdownCheckTime += 60 * 60 * 1000; - } - LOG.info("Long-polling for work ({} second timeout).", POLL_TIMEOUT / 1000.0); - // Long-poll (wait a few seconds for messages to become available) - List tasks = getSomeWork(WorkType.BATCH); - if (tasks == null) { - LOG.info("Didn't get any work. Retrying."); - idle = true; - continue; - } - - // run through high-priority tasks first to ensure they are enqueued even if the batch - // queue blocks. - tasks.stream().filter(t -> t.outputLocation == null) - .forEach(t -> highPriorityExecutor.execute(() -> { - LOG.warn( - "Handling single point request via normal channel, side channel should open shortly."); - this.handleOneRequest(t); - })); - - logQueueStatus(); - - // enqueue low-priority tasks; note that this may block anywhere in the process - tasks.stream().filter(t -> t.outputLocation != null) - .forEach(t -> { - // attempt to enqueue, waiting if the queue is full - while (true) { - try { - batchExecutor.execute(() -> this.handleOneRequest(t)); - break; - } catch (RejectedExecutionException e) { - // queue is full, wait 200ms and try again - try { - Thread.sleep(200); - } catch (InterruptedException e1) { /* nothing */} - } - } - }); - - logQueueStatus(); - - idle = false; - } - } - - /** - * This is the callback that processes a single task and returns the results upon completion. - * It may be called several times simultaneously on different executor threads. - */ - private void handleOneRequest(AnalystClusterRequest clusterRequest) { - - if (dryRunFailureRate >= 0) { - // This worker is running in test mode. - // It should report all work as completed without actually doing anything, - // but will fail a certain percentage of the time. - if (random.nextInt(100) >= dryRunFailureRate) { - // Pretend to succeed. - deleteRequest(clusterRequest); - } else { - LOG.info("Intentionally failing on task {}", clusterRequest.taskId); - } - return; - } - - try { - long startTime = System.currentTimeMillis(); - LOG.info("Handling message {}", clusterRequest.toString()); - - // We need to distinguish between and handle four different types of requests here: - // Either vector isochrones or accessibility to a pointset, - // as either a single-origin priority request (where the result is returned immediately) - // or a job task (where the result is saved to output location on S3). - boolean isochrone = (clusterRequest.destinationPointsetId == null); - boolean singlePoint = (clusterRequest.outputLocation == null); - boolean transit = (clusterRequest.profileRequest.transitModes != null && clusterRequest.profileRequest.transitModes.isTransit()); - - if (singlePoint) { - lastHighPriorityRequestProcessed = startTime; - if (!sideChannelOpen) { - openSideChannel(); - } - } - - TaskStatistics ts = new TaskStatistics(); - ts.pointsetId = clusterRequest.destinationPointsetId; - ts.graphId = clusterRequest.graphId; - ts.awsInstanceType = instanceType; - ts.jobId = clusterRequest.jobId; - ts.workerId = machineId; - ts.single = singlePoint; - - // Get the graph object for the ID given in the request, fetching inputs and building as needed. - // All requests handled together are for the same graph, and this call is synchronized so the graph will - // only be built once. - long graphStartTime = System.currentTimeMillis(); - Graph graph = clusterGraphBuilder.getGraph(clusterRequest.graphId); - graphId = clusterRequest.graphId; // Record graphId so we "stick" to this same graph on subsequent polls - ts.graphBuild = (int) (System.currentTimeMillis() - graphStartTime); - ts.graphTripCount = graph.index.patternForTrip.size(); - ts.graphStopCount = graph.index.stopForId.size(); - ts.lon = clusterRequest.profileRequest.fromLon; - ts.lat = clusterRequest.profileRequest.fromLat; - - final SampleSet sampleSet; - - // If this one-to-many request is for accessibility information based on travel times to a pointset, - // fetch the set of points we will use as destinations. - if (isochrone) { - // This is an isochrone request, tell the RepeatedRaptorProfileRouter there are no targets. - sampleSet = null; - } else { - // This is not an isochrone request. There is necessarily a destination point set supplied. - PointSet pointSet = pointSetDatastore.get(clusterRequest.destinationPointsetId); - sampleSet = pointSet.getOrCreateSampleSet(graph); // TODO this breaks if graph has been rebuilt - } - - // Note that all parameters to create the Raptor worker data are passed in the constructor except ts. - // Why not pass in ts as well since this is a throwaway calculator? - RepeatedRaptorProfileRouter router = - new RepeatedRaptorProfileRouter(graph, clusterRequest.profileRequest, sampleSet); - router.ts = ts; - - // Produce RAPTOR data tables, going through a cache where relevant. - // This is only used for multi-point requests. Single-point requests are assumed to be continually - // changing, so we create throw-away RAPTOR tables for them. - // Ideally we'd want this cacheing to happen transparently inside the RepeatedRaptorProfileRouter, - // but the RepeatedRaptorProfileRouter doesn't know the job ID or other information from the cluster request. - // It would be possible to just supply the cache _key_ as a way of saying that the cache should be used. - // But then we'd need to pass in both the cache and the key, which is weird. - if (transit && !singlePoint) { - long dataStart = System.currentTimeMillis(); - router.raptorWorkerData = workerDataCache.get(clusterRequest.jobId, () -> RepeatedRaptorProfileRouter - .getRaptorWorkerData(clusterRequest.profileRequest, graph, sampleSet, ts)); - ts.raptorData = (int) (System.currentTimeMillis() - dataStart); - } else { - // The worker will generate a one-time throw-away table. - router.raptorWorkerData = null; - } - - // Run the core repeated-raptor analysis. - // This result envelope will contain the results of the one-to-many profile or single-departure-time search. - ResultEnvelope envelope = new ResultEnvelope(); - try { - // TODO when router runs, if there are no transit modes defined it should just skip the transit work. - router.includeTimes = clusterRequest.includeTimes; - envelope = router.route(); - envelope.id = clusterRequest.id; - ts.success = true; - } catch (Exception ex) { - // An error occurred. Leave the envelope empty and TODO include error information. - LOG.error("Error occurred in profile request", ex); - ts.success = false; - } - - // Send the ResultEnvelope back to the user. - // The results are either stored on S3 (for multi-origin jobs) or sent back through the broker (for - // immediate interactive display of isochrones). - envelope.id = clusterRequest.id; - envelope.jobId = clusterRequest.jobId; - envelope.destinationPointsetId = clusterRequest.destinationPointsetId; - if (clusterRequest.outputLocation != null) { - // Convert the result envelope and its contents to JSON and gzip it in this thread. - // Transfer the results to Amazon S3 in another thread, piping between the two. - String s3key = String.join("/", clusterRequest.jobId, clusterRequest.id + ".json.gz"); - PipedInputStream inPipe = new PipedInputStream(); - PipedOutputStream outPipe = new PipedOutputStream(inPipe); - new Thread(() -> { - s3.putObject(clusterRequest.outputLocation, s3key, inPipe, null); - }).start(); - OutputStream gzipOutputStream = new GZIPOutputStream(outPipe); - // We could do the writeValue() in a thread instead, in which case both the DELETE and S3 options - // could consume it in the same way. - objectMapper.writeValue(gzipOutputStream, envelope); - gzipOutputStream.close(); - // Tell the broker the task has been handled and should not be re-delivered to another worker. - deleteRequest(clusterRequest); - } else { - // No output location was provided. Instead of saving the result on S3, - // return the result immediately via a connection held open by the broker and mark the task completed. - finishPriorityTask(clusterRequest, envelope); - } - - // Record information about the current task so we can analyze usage and efficiency over time. - ts.total = (int) (System.currentTimeMillis() - startTime); - statsStore.store(ts); - - } catch (Exception ex) { - LOG.error("An error occurred while routing", ex); - } - - } - - /** Open a single point channel to the broker to receive high-priority requests immediately */ - private synchronized void openSideChannel () { - if (sideChannelOpen) { - return; - } - LOG.info("Opening side channel for single point requests."); - new Thread(() -> { - sideChannelOpen = true; - // don't keep single point connections alive forever - while (System.currentTimeMillis() < lastHighPriorityRequestProcessed + SINGLE_POINT_KEEPALIVE) { - LOG.info("Awaiting high-priority work"); - try { - List tasks = getSomeWork(WorkType.HIGH_PRIORITY); - - if (tasks != null) - tasks.stream().forEach(t -> highPriorityExecutor.execute( - () -> this.handleOneRequest(t))); - - logQueueStatus(); - } catch (Exception e) { - LOG.error("Unexpected exception getting single point work", e); - } - } - sideChannelOpen = false; - }).start(); - } - - public List getSomeWork(WorkType type) { - - // Run a POST request (long-polling for work) indicating which graph this worker prefers to work on - String url; - if (type == WorkType.HIGH_PRIORITY) { - // this is a side-channel request for single point work - url = BROKER_BASE_URL + "/single/" + graphId; - } else { - url = BROKER_BASE_URL + "/dequeue/" + graphId; - } - HttpPost httpPost = new HttpPost(url); - httpPost.setHeader(new BasicHeader(WORKER_ID_HEADER, machineId)); - HttpResponse response = null; - try { - response = httpClient.execute(httpPost); - HttpEntity entity = response.getEntity(); - if (entity == null) { - return null; - } - if (response.getStatusLine().getStatusCode() != 200) { - EntityUtils.consumeQuietly(entity); - return null; - } - return objectMapper.readValue(entity.getContent(), new TypeReference>() { - }); - } catch (JsonProcessingException e) { - LOG.error("JSON processing exception while getting work", e); - } catch (SocketTimeoutException stex) { - LOG.error("Socket timeout while waiting to receive work."); - } catch (HttpHostConnectException ce) { - LOG.error("Broker refused connection. Sleeping before retry."); - try { - Thread.currentThread().sleep(5000); - } catch (InterruptedException e) { - } - } catch (IOException e) { - LOG.error("IO exception while getting work", e); - } - return null; - - } - - /** - * Signal the broker that the given high-priority task is completed, providing a result. - */ - public void finishPriorityTask(AnalystClusterRequest clusterRequest, Object result) { - String url = BROKER_BASE_URL + String.format("/complete/priority/%s", clusterRequest.taskId); - HttpPost httpPost = new HttpPost(url); - try { - // TODO reveal any errors etc. that occurred on the worker. - // Really this should probably be done with an InputStreamEntity and a JSON writer thread. - byte[] serializedResult = objectMapper.writeValueAsBytes(result); - httpPost.setEntity(new ByteArrayEntity(serializedResult)); - HttpResponse response = httpClient.execute(httpPost); - // Signal the http client library that we're done with this response object, allowing connection reuse. - EntityUtils.consumeQuietly(response.getEntity()); - if (response.getStatusLine().getStatusCode() == 200) { - LOG.info("Successfully marked task {} as completed.", clusterRequest.taskId); - } else if (response.getStatusLine().getStatusCode() == 404) { - LOG.info("Task {} was not marked as completed because it doesn't exist.", clusterRequest.taskId); - } else { - LOG.info("Failed to mark task {} as completed, ({}).", clusterRequest.taskId, - response.getStatusLine()); - } - } catch (Exception e) { - LOG.warn("Failed to mark task {} as completed.", clusterRequest.taskId, e); - } - } - - /** - * Tell the broker that the given message has been successfully processed by a worker (HTTP DELETE). - */ - public void deleteRequest(AnalystClusterRequest clusterRequest) { - String url = BROKER_BASE_URL + String.format("/tasks/%s", clusterRequest.taskId); - HttpDelete httpDelete = new HttpDelete(url); - try { - HttpResponse response = httpClient.execute(httpDelete); - // Signal the http client library that we're done with this response object, allowing connection reuse. - EntityUtils.consumeQuietly(response.getEntity()); - if (response.getStatusLine().getStatusCode() == 200) { - LOG.info("Successfully deleted task {}.", clusterRequest.taskId); - } else { - LOG.info("Failed to delete task {} ({}).", clusterRequest.taskId, response.getStatusLine()); - } - } catch (Exception e) { - LOG.warn("Failed to delete task {}", clusterRequest.taskId, e); - } - } - - /** Get the AWS instance type if applicable */ - public String getInstanceType () { - try { - HttpGet get = new HttpGet(); - // see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html - // This seems very much not EC2-like to hardwire an IP address for getting instance metadata, - // but that's how it's done. - get.setURI(new URI("http://169.254.169.254/latest/meta-data/instance-type")); - get.setConfig(RequestConfig.custom() - .setConnectTimeout(2000) - .setSocketTimeout(2000) - .build() - ); - - HttpResponse res = httpClient.execute(get); - - InputStream is = res.getEntity().getContent(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - String type = reader.readLine().trim(); - reader.close(); - return type; - } catch (Exception e) { - LOG.info("could not retrieve EC2 instance type, you may be running outside of EC2."); - return null; - } - } - - /** log queue status */ - private void logQueueStatus() { - LOG.info("Waiting tasks: high priority: {}, batch: {}", highPriorityExecutor.getQueue().size(), batchExecutor.getQueue().size()); - } - - /** - * Requires a worker configuration, which is a Java Properties file with the following - * attributes. - * - * broker-address address of the broker, without protocol or port - * broker port port broker is running on, default 80. - * graphs-bucket S3 bucket in which graphs are stored. - * pointsets-bucket S3 bucket in which pointsets are stored - * auto-shutdown Should this worker shut down its machine if it is idle (e.g. on throwaway cloud instances) - * statistics-queue SQS queue to which to send statistics (optional) - * initial-graph-id The graph ID for this worker to start on - */ - public static void main(String[] args) { - LOG.info("Starting analyst worker"); - LOG.info("OTP commit is {}", MavenVersion.VERSION.commit); - - Properties config = new Properties(); - - try { - File cfg; - if (args.length > 0) - cfg = new File(args[0]); - else - cfg = new File("worker.conf"); - - InputStream cfgis = new FileInputStream(cfg); - config.load(cfgis); - cfgis.close(); - } catch (Exception e) { - LOG.info("Error loading worker configuration", e); - return; - } - - if (Boolean.parseBoolean(config.getProperty("use-transport-networks", "false"))) { - // start R5 to work with transport networks - LOG.info("Transport network support enabled, deferring computation to R5"); - com.conveyal.r5.analyst.cluster.AnalystWorker.main(args); - } - else { - try { - new AnalystWorker(config).run(); - } catch (Exception e) { - LOG.error("Error in analyst worker", e); - return; - } - } - } - - public static enum WorkType { - HIGH_PRIORITY, BATCH; - } -} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/analyst/cluster/ClusterGraphBuilder.java b/src/main/java/org/opentripplanner/analyst/cluster/ClusterGraphBuilder.java deleted file mode 100644 index 2918981cf82..00000000000 --- a/src/main/java/org/opentripplanner/analyst/cluster/ClusterGraphBuilder.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.opentripplanner.analyst.cluster; - -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.S3Object; -import org.apache.commons.io.IOUtils; -import org.opentripplanner.graph_builder.GraphBuilder; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.standalone.CommandLineParameters; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * Builds and caches graphs as well as the inputs they are built from for use in Analyst Cluster workers. - */ -public class ClusterGraphBuilder { - - private static final Logger LOG = LoggerFactory.getLogger(ClusterGraphBuilder.class); - - private AmazonS3Client s3 = new AmazonS3Client(); - - private static final String GRAPH_CACHE_DIR = "graph_cache"; - - private final String graphBucket; - - String currGraphId = null; - - Graph currGraph = null; - - public ClusterGraphBuilder (String graphBucket) { - this.graphBucket = graphBucket; - } - - /** - * Return the graph for the given unique identifier for graph builder inputs on S3. - * If this is the same as the last graph built, just return the pre-built graph. - * If not, build the graph from the inputs, fetching them from S3 to the local cache as needed. - */ - public synchronized Graph getGraph(String graphId) { - - LOG.info("Finding a graph for ID {}", graphId); - - if (graphId.equals(currGraphId)) { - LOG.info("GraphID has not changed. Reusing the last graph that was built."); - return currGraph; - } - - // The location of the inputs that will be used to build this graph - File graphDataDirectory = new File(GRAPH_CACHE_DIR, graphId); - - // If we don't have a local copy of the inputs, fetch graph data as a ZIP from S3 and unzip it - if( ! graphDataDirectory.exists() || graphDataDirectory.list().length == 0) { - LOG.info("Downloading graph input files."); - graphDataDirectory.mkdirs(); - S3Object graphDataZipObject = s3.getObject(graphBucket, graphId + ".zip"); - ZipInputStream zis = new ZipInputStream(graphDataZipObject.getObjectContent()); - try { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - File entryDestination = new File(graphDataDirectory, entry.getName()); - // Are both these mkdirs calls necessary? - entryDestination.getParentFile().mkdirs(); - if (entry.isDirectory()) - entryDestination.mkdirs(); - else { - OutputStream entryFileOut = new FileOutputStream(entryDestination); - IOUtils.copy(zis, entryFileOut); - entryFileOut.close(); - } - } - zis.close(); - } catch (Exception e) { - // TODO delete graph cache dir which is probably corrupted - LOG.info("Error retrieving graph files", e); - } - } else { - LOG.info("Graph input files were found locally. Using these files from the cache."); - } - - // Now we have a local copy of these graph inputs. Make a graph out of them. - CommandLineParameters params = new CommandLineParameters(); - params.build = new File(GRAPH_CACHE_DIR, graphId); - params.inMemory = true; - GraphBuilder graphBuilder = GraphBuilder.forDirectory(params, params.build); - graphBuilder.run(); - Graph graph = graphBuilder.getGraph(); - graph.routerId = graphId; - // re-index the graph to ensure all data is added and recreate a new streetIndex. It's ok to recreate the - // streetIndex because the previous one created during graph build is not needed anymore and isn't able to be - // used outside of the graphBuilder. - graph.index(true); - graph.index.clusterStopsAsNeeded(); - this.currGraphId = graphId; - this.currGraph = graph; - return graph; - - } - -} diff --git a/src/main/java/org/opentripplanner/analyst/cluster/ClusterGraphService.java b/src/main/java/org/opentripplanner/analyst/cluster/ClusterGraphService.java deleted file mode 100644 index dc57ca137a1..00000000000 --- a/src/main/java/org/opentripplanner/analyst/cluster/ClusterGraphService.java +++ /dev/null @@ -1,334 +0,0 @@ -package org.opentripplanner.analyst.cluster; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.profile.ProfileCredentialsProvider; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.S3Object; -import com.google.common.collect.Maps; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.opentripplanner.graph_builder.GraphBuilder; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.services.GraphService; -import org.opentripplanner.routing.services.GraphSource; -import org.opentripplanner.routing.services.GraphSource.Factory; -import org.opentripplanner.standalone.CommandLineParameters; -import org.opentripplanner.standalone.Router; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.util.Collection; -import java.util.Enumeration; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipException; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -// TODO does not really need to extend GraphService -public class ClusterGraphService extends GraphService { - - static File GRAPH_DIR = new File("cache", "graphs"); - - private String graphBucket; - - private Boolean workOffline = false; - private AmazonS3Client s3; - - private static final Logger LOG = LoggerFactory.getLogger(GraphService.class); - - // don't use more than 60% of free memory to cache graphs - private Map graphMap = Maps.newConcurrentMap(); - - @Override - public synchronized Router getRouter(String graphId) { - - GRAPH_DIR.mkdirs(); - - if(!graphMap.containsKey(graphId)) { - - try { - if (!bucketCached(graphId)) { - if(!workOffline) { - downloadGraphSourceFiles(graphId, GRAPH_DIR); - } - } - } catch (IOException e) { - LOG.error("exception finding graph {}", graphId, e); - } - - CommandLineParameters params = new CommandLineParameters(); - params.build = new File(GRAPH_DIR, graphId); - params.inMemory = true; - GraphBuilder gbt = GraphBuilder.forDirectory(params, params.build); - gbt.run(); - - Graph g = gbt.getGraph(); - - g.routerId = graphId; - - // re-index the graph to ensure all data is added and recreate a new streetIndex. It's ok to recreate the - // streetIndex because the previous one created during graph build is not needed anymore and isn't able to - // be used outside of the graphBuilder. - g.index(true); - - g.index.clusterStopsAsNeeded(); - - Router r = new Router(graphId, g); - - // temporarily disable graph caching so we don't run out of RAM. - // Long-term we will use an actual cache for this. - //graphMap.put(graphId,r); - - return r; - - } - else { - return graphMap.get(graphId); - } - } - - public ClusterGraphService(String s3CredentialsFilename, Boolean workOffline, String bucket) { - - if(!workOffline) { - if (s3CredentialsFilename != null) { - AWSCredentials creds = new ProfileCredentialsProvider(s3CredentialsFilename, "default").getCredentials(); - s3 = new AmazonS3Client(creds); - } - else { - // This will first check for credentials in environment variables or ~/.aws/credentials - // then fall back on S3 credentials propagated to EC2 instances via IAM roles. - s3 = new AmazonS3Client(); - } - - this.graphBucket = bucket; - } - - this.workOffline = workOffline; - } - - // adds either a zip file or graph directory to S3, or local cache for offline use - public void addGraphFile(File graphFile) throws IOException { - - String graphId = graphFile.getName(); - - if(graphId.endsWith(".zip")) - graphId = graphId.substring(0, graphId.length() - 4); - - File graphDir = new File(GRAPH_DIR, graphId); - - if (graphDir.exists()) { - if (graphDir.list().length == 0) { - graphDir.delete(); - } - else { - return; - } - } - - // if we're here the directory has either been deleted or never existed - graphDir.mkdirs(); - - File graphDataZip = new File(GRAPH_DIR, graphId + ".zip"); - - // if directory zip contents store as zip - // either way ensure there is an extracted copy in the local cache - if(graphFile.isDirectory()) { - FileUtils.copyDirectory(graphFile, graphDir); - - zipGraphDir(graphDir, graphDataZip); - } - else if(graphFile.getName().endsWith(".zip")) { - FileUtils.copyFile(graphFile, graphDataZip); - unpackGraphZip(graphDataZip, graphDir, false); - } - else { - graphDataZip = null; - } - - if(!workOffline && graphDataZip != null) { - // only upload if it's not there already - try { - s3.getObject(graphBucket, graphId + ".zip"); - } catch (AmazonServiceException e) { - s3.putObject(graphBucket, graphId+".zip", graphDataZip); - } - } - - graphDataZip.delete(); - - } - - public synchronized File getZippedGraph(String graphId) throws IOException { - - File graphDataDir = new File(GRAPH_DIR, graphId); - - File graphZipFile = new File(GRAPH_DIR, graphId + ".zip"); - - if(!graphDataDir.exists() && graphDataDir.isDirectory()) { - - FileOutputStream fileOutputStream = new FileOutputStream(graphZipFile); - ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream); - - byte[] buffer = new byte[1024]; - - for(File f : graphDataDir.listFiles()) { - ZipEntry zipEntry = new ZipEntry(f.getName()); - zipOutputStream.putNextEntry(zipEntry); - FileInputStream fileInput = new FileInputStream(f); - - int len; - while ((len = fileInput.read(buffer)) > 0) { - zipOutputStream.write(buffer, 0, len); - } - - fileInput.close(); - zipOutputStream.closeEntry(); - } - - zipOutputStream.close(); - - return graphZipFile; - - } - - return null; - - } - - private static boolean bucketCached(String graphId) throws IOException { - File graphData = new File(GRAPH_DIR, graphId); - - // check if cached but only as zip - if(!graphData.exists()) { - File graphDataZip = new File(GRAPH_DIR, graphId + ".zip"); - - if(graphDataZip.exists()) { - zipGraphDir(graphData, graphDataZip); - } - } - - - return graphData.exists() && graphData.isDirectory(); - } - - private void downloadGraphSourceFiles(String graphId, File dir) throws IOException { - - File graphCacheDir = dir; - if (!graphCacheDir.exists()) - graphCacheDir.mkdirs(); - - File graphZipFile = new File(graphCacheDir, graphId + ".zip"); - - File extractedGraphDir = new File(graphCacheDir, graphId); - - if (extractedGraphDir.exists()) { - FileUtils.deleteDirectory(extractedGraphDir); - } - - extractedGraphDir.mkdirs(); - - S3Object graphZip = s3.getObject(graphBucket, graphId+".zip"); - - InputStream zipFileIn = graphZip.getObjectContent(); - - OutputStream zipFileOut = new FileOutputStream(graphZipFile); - - IOUtils.copy(zipFileIn, zipFileOut); - IOUtils.closeQuietly(zipFileIn); - IOUtils.closeQuietly(zipFileOut); - - unpackGraphZip(graphZipFile, extractedGraphDir); - } - - private static void unpackGraphZip(File graphZipFile, File extractedGraphDir) throws ZipException, IOException { - // delete by default - unpackGraphZip(graphZipFile, extractedGraphDir, true); - } - - private static void unpackGraphZip(File graphZipFile, File extractedGraphDir, boolean delete) throws ZipException, IOException { - - ZipFile zipFile = new ZipFile(graphZipFile); - - Enumeration entries = zipFile.entries(); - - while (entries.hasMoreElements()) { - - ZipEntry entry = entries.nextElement(); - File entryDestination = new File(extractedGraphDir, entry.getName()); - - entryDestination.getParentFile().mkdirs(); - - if (entry.isDirectory()) - entryDestination.mkdirs(); - else { - InputStream entryFileIn = zipFile.getInputStream(entry); - OutputStream entryFileOut = new FileOutputStream(entryDestination); - IOUtils.copy(entryFileIn, entryFileOut); - IOUtils.closeQuietly(entryFileIn); - IOUtils.closeQuietly(entryFileOut); - } - } - - zipFile.close(); - - if (delete) { - graphZipFile.delete(); - } - } - - private static void zipGraphDir(File graphDirectory, File zipGraphFile) throws IOException { - - FileOutputStream fileOutputStream = new FileOutputStream(zipGraphFile); - ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream); - - byte[] buffer = new byte[1024]; - - for(File f : graphDirectory.listFiles()) { - if (f.isDirectory()) - continue; - - ZipEntry zipEntry = new ZipEntry(f.getName()); - zipOutputStream.putNextEntry(zipEntry); - FileInputStream fileInput = new FileInputStream(f); - - int len; - while ((len = fileInput.read(buffer)) > 0) { - zipOutputStream.write(buffer, 0, len); - } - - fileInput.close(); - zipOutputStream.closeEntry(); - } - - zipOutputStream.close(); - } - - - @Override - public int evictAll() { - graphMap.clear(); - return 0; - } - - @Override - public Collection getRouterIds() { - return graphMap.keySet(); - } - - @Override - public Factory getGraphSourceFactory() { - return null; - } - - @Override - public boolean registerGraph(String arg0, GraphSource arg1) { - return false; - } - - @Override - public void setDefaultRouterId(String arg0) { - } -} diff --git a/src/main/java/org/opentripplanner/analyst/cluster/PointSetDatastore.java b/src/main/java/org/opentripplanner/analyst/cluster/PointSetDatastore.java deleted file mode 100644 index 645c835d137..00000000000 --- a/src/main/java/org/opentripplanner/analyst/cluster/PointSetDatastore.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.opentripplanner.analyst.cluster; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.profile.ProfileCredentialsProvider; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.S3Object; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.io.ByteStreams; -import org.apache.commons.io.FileUtils; -import org.opentripplanner.analyst.PointSet; -import org.opentripplanner.analyst.PointSetCache; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.List; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** - * TODO what does this do? Does it really need to be a subclass? - */ -public class PointSetDatastore extends PointSetCache { - - static private File POINT_DIR = new File("cache", "pointsets"); - private String pointsetBucket; - - private AmazonS3Client s3; - private final Boolean workOffline; - - public PointSetDatastore(Integer maxCacheSize, String s3CredentialsFilename, - Boolean workOffline, String pointsetBucket){ - - super(); - - // allow the data store to work offline with cached data and skip S3 connection - this.workOffline = workOffline; - - this.pointsetBucket = pointsetBucket; - - if(!this.workOffline) { - if (s3CredentialsFilename != null) { - AWSCredentials creds = new ProfileCredentialsProvider(s3CredentialsFilename, "default").getCredentials(); - s3 = new AmazonS3Client(creds); - } - else { - // default credentials providers, e.g. IAM role - s3 = new AmazonS3Client(); - } - } - - // set up the cache - this.pointSets = CacheBuilder.newBuilder() - .maximumSize(maxCacheSize) - .build(new S3PointSetLoader(workOffline, s3, pointsetBucket)); - } - - // adds file to S3 Data store or offline cache (if working offline) - public String addPointSet(File pointSetFile, String pointSetId) throws IOException { - if (pointSetId == null) - throw new NullPointerException("null point set id"); - - File renamedPointSetFile = new File(POINT_DIR, pointSetId + ".json"); - - if (renamedPointSetFile.exists()) - return pointSetId; - - FileUtils.copyFile(pointSetFile, renamedPointSetFile); - - if(!this.workOffline) { - // only upload if it doesn't exist - try { - s3.getObjectMetadata(pointsetBucket, pointSetId + ".json.gz"); - } catch (AmazonServiceException e) { - // gzip compression in storage, not because we're worried about file size but to speed file transfer - FileInputStream fis = new FileInputStream(pointSetFile); - File tempFile = File.createTempFile(pointSetId, ".json.gz"); - FileOutputStream fos = new FileOutputStream(tempFile); - GZIPOutputStream gos = new GZIPOutputStream(fos); - - try { - ByteStreams.copy(fis, gos); - } finally { - gos.close(); - fis.close(); - } - - s3.putObject(pointsetBucket, pointSetId + ".json.gz", tempFile); - tempFile.delete(); - } - } - - return pointSetId; - - } - - /** does this pointset exist in local cache? */ - public boolean isCached (String pointsetId) { - return new File(POINT_DIR, pointsetId + ".json").exists(); - } - - /** - * Load pointsets from S3. - */ - protected static class S3PointSetLoader extends CacheLoader { - - private Boolean workOffline; - private AmazonS3Client s3; - private String pointsetBucket; - - /** - * Construct a new point set loader. S3 clients are generally threadsafe, so it's fine to share them. - */ - public S3PointSetLoader(Boolean workOffline, AmazonS3Client s3, String pointsetBucket) { - this.workOffline = workOffline; - this.s3 = s3; - this.pointsetBucket = pointsetBucket; - } - - @Override - public PointSet load (String pointSetId) throws Exception { - - File cachedFile; - - if(!workOffline) { - // get pointset metadata from S3 - cachedFile = new File(POINT_DIR, pointSetId + ".json"); - if(!cachedFile.exists()){ - POINT_DIR.mkdirs(); - - S3Object obj = s3.getObject(pointsetBucket, pointSetId + ".json.gz"); - ObjectMetadata objMet = obj.getObjectMetadata(); - FileOutputStream fos = new FileOutputStream(cachedFile); - GZIPInputStream gis = new GZIPInputStream(obj.getObjectContent()); - try { - ByteStreams.copy(gis, fos); - } finally { - fos.close(); - gis.close(); - } - } - } - else - cachedFile = new File(POINT_DIR, pointSetId + ".json"); - - - - // grab it from the cache - - return PointSet.fromGeoJson(cachedFile); - } - } - - @Override - public List getPointSetIds() { - // we have no clue what is in the S3 bucket. - throw new UnsupportedOperationException("S3-backed point set datastore does not know what pointsets are available."); - } -} diff --git a/src/main/java/org/opentripplanner/analyst/cluster/SQSTaskStatisticsStore.java b/src/main/java/org/opentripplanner/analyst/cluster/SQSTaskStatisticsStore.java deleted file mode 100644 index d2599cc3803..00000000000 --- a/src/main/java/org/opentripplanner/analyst/cluster/SQSTaskStatisticsStore.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.opentripplanner.analyst.cluster; - -import com.amazonaws.handlers.AsyncHandler; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.sqs.AmazonSQSAsync; -import com.amazonaws.services.sqs.AmazonSQSAsyncClient; -import com.amazonaws.services.sqs.buffered.AmazonSQSBufferedAsyncClient; -import com.amazonaws.services.sqs.model.SendMessageRequest; -import com.amazonaws.services.sqs.model.SendMessageResult; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A Task Statistics Store that throws things into SQS. - */ -public class SQSTaskStatisticsStore implements TaskStatisticsStore { - private static final Logger LOG = LoggerFactory.getLogger(SQSTaskStatisticsStore.class); - - private String queueUrl; - - private ObjectMapper objectMapper = new ObjectMapper(); - - private AmazonSQSAsync sqs = new AmazonSQSBufferedAsyncClient(new AmazonSQSAsyncClient()); - - /** create a task statistics store for the given queue name */ - public SQSTaskStatisticsStore(String queueName) { - Region current = Regions.getCurrentRegion(); - - if (current != null) { - LOG.info("Assuming statistics queue is in region {}", current); - sqs.setEndpoint("sqs." + current.toString() + ".amazonaws.com"); - } - - try { - queueUrl = sqs.getQueueUrl(queueName).getQueueUrl(); - } catch (Exception e) { - LOG.error("Unable to initialize statistics store", e); - } - LOG.info("Sending statistics to SQS queue {}", queueName); - } - - public void store (TaskStatistics ts) { - try { - String json = objectMapper.writeValueAsString(ts); - - SendMessageRequest req = new SendMessageRequest(); - req.setMessageBody(json); - req.setQueueUrl(queueUrl); - - sqs.sendMessageAsync(req, new AsyncHandler() { - @Override public void onError(Exception e) { - LOG.error("Error saving stats to SQS", e); - } - - @Override public void onSuccess(SendMessageRequest request, - SendMessageResult sendMessageResult) { - /* do nothing */ - } - }); - } catch (Exception e) { - LOG.error("Error saving stats to SQS", e); - } - } -} diff --git a/src/main/java/org/opentripplanner/analyst/cluster/WorkerIdDefiner.java b/src/main/java/org/opentripplanner/analyst/cluster/WorkerIdDefiner.java deleted file mode 100644 index 51fc86f38ef..00000000000 --- a/src/main/java/org/opentripplanner/analyst/cluster/WorkerIdDefiner.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.opentripplanner.analyst.cluster; - -import ch.qos.logback.core.PropertyDefinerBase; - -/** - * A class that allows the logging framework to access the worker ID; with a custom logback config - * this can be used to print the machine ID with each log message. This is useful if you have multiple - * workers logging to the same log aggregation service. - * - * This does seem like it should be a static class of AnalystWorker, but AnalystWorker needs this - * class loaded to initialize its logger which is a static field, so it would have to be at the top - * of the file, above the logger definition, which is ugly and confusing so we leave it as its own - * bona fide class. - * - * It would seem that Mapped Diagnostic Contexts would be ideal for this purpose, but they are - * thread-scoped, and computation takes place in multiple threads; we need this to be JVM-scoped. - */ -public class WorkerIdDefiner extends PropertyDefinerBase { - @Override public String getPropertyValue() { - return AnalystWorker.machineId; - } -} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/analyst/core/GeometryIndex.java b/src/main/java/org/opentripplanner/analyst/core/GeometryIndex.java index 8c5325c6e56..d582cbcc9c9 100644 --- a/src/main/java/org/opentripplanner/analyst/core/GeometryIndex.java +++ b/src/main/java/org/opentripplanner/analyst/core/GeometryIndex.java @@ -19,9 +19,9 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Maps; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.index.strtree.STRtree; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.index.strtree.STRtree; /** * This index is used in Analyst and does not need to be instantiated if you are not performing diff --git a/src/main/java/org/opentripplanner/analyst/core/GeometryIndexService.java b/src/main/java/org/opentripplanner/analyst/core/GeometryIndexService.java index 5fd781a315a..200ba398238 100644 --- a/src/main/java/org/opentripplanner/analyst/core/GeometryIndexService.java +++ b/src/main/java/org/opentripplanner/analyst/core/GeometryIndexService.java @@ -5,7 +5,7 @@ import org.opengis.geometry.BoundingBox; import org.opengis.referencing.crs.CoordinateReferenceSystem; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public interface GeometryIndexService { diff --git a/src/main/java/org/opentripplanner/analyst/core/IsochroneData.java b/src/main/java/org/opentripplanner/analyst/core/IsochroneData.java index 90fb97ee0aa..98c28d040be 100644 --- a/src/main/java/org/opentripplanner/analyst/core/IsochroneData.java +++ b/src/main/java/org/opentripplanner/analyst/core/IsochroneData.java @@ -1,7 +1,7 @@ package org.opentripplanner.analyst.core; import com.fasterxml.jackson.annotation.JsonProperty; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; import java.io.Serializable; diff --git a/src/main/java/org/opentripplanner/analyst/request/IsoChroneRequest.java b/src/main/java/org/opentripplanner/analyst/request/IsoChroneRequest.java index 29103dc1e3f..f67be376715 100644 --- a/src/main/java/org/opentripplanner/analyst/request/IsoChroneRequest.java +++ b/src/main/java/org/opentripplanner/analyst/request/IsoChroneRequest.java @@ -3,7 +3,7 @@ import java.util.Arrays; import java.util.List; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * A request for an isochrone vector. diff --git a/src/main/java/org/opentripplanner/analyst/request/IsoChroneSPTRendererRecursiveGrid.java b/src/main/java/org/opentripplanner/analyst/request/IsoChroneSPTRendererRecursiveGrid.java index c296d84d980..fd34f1b3b31 100644 --- a/src/main/java/org/opentripplanner/analyst/request/IsoChroneSPTRendererRecursiveGrid.java +++ b/src/main/java/org/opentripplanner/analyst/request/IsoChroneSPTRendererRecursiveGrid.java @@ -19,7 +19,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * Compute isochrones out of a shortest path tree request (RecursiveGrid isoline algorithm). diff --git a/src/main/java/org/opentripplanner/analyst/request/SampleFactory.java b/src/main/java/org/opentripplanner/analyst/request/SampleFactory.java index d932095bb5b..d40cba3fca7 100644 --- a/src/main/java/org/opentripplanner/analyst/request/SampleFactory.java +++ b/src/main/java/org/opentripplanner/analyst/request/SampleFactory.java @@ -1,10 +1,10 @@ package org.opentripplanner.analyst.request; import com.google.common.collect.Iterables; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineString; import gnu.trove.map.TIntDoubleMap; import gnu.trove.map.hash.TIntDoubleHashMap; import org.opentripplanner.analyst.core.Sample; diff --git a/src/main/java/org/opentripplanner/analyst/request/SampleGridRenderer.java b/src/main/java/org/opentripplanner/analyst/request/SampleGridRenderer.java index b03f60c97cb..dc7cc9a7d10 100644 --- a/src/main/java/org/opentripplanner/analyst/request/SampleGridRenderer.java +++ b/src/main/java/org/opentripplanner/analyst/request/SampleGridRenderer.java @@ -25,7 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * Compute a sample grid from a SPT request. diff --git a/src/main/java/org/opentripplanner/analyst/request/SampleGridRequest.java b/src/main/java/org/opentripplanner/analyst/request/SampleGridRequest.java index 8690ffe37f0..9d3ff0b72fa 100644 --- a/src/main/java/org/opentripplanner/analyst/request/SampleGridRequest.java +++ b/src/main/java/org/opentripplanner/analyst/request/SampleGridRequest.java @@ -1,6 +1,6 @@ package org.opentripplanner.analyst.request; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * A request for a sample grid (of a SPT). diff --git a/src/main/java/org/opentripplanner/analyst/scenario/AddTripPattern.java b/src/main/java/org/opentripplanner/analyst/scenario/AddTripPattern.java index c6cb419023b..f1dba73e18d 100644 --- a/src/main/java/org/opentripplanner/analyst/scenario/AddTripPattern.java +++ b/src/main/java/org/opentripplanner/analyst/scenario/AddTripPattern.java @@ -1,12 +1,11 @@ package org.opentripplanner.analyst.scenario; -import com.conveyal.gtfs.model.Route; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; -import org.apache.commons.math3.analysis.function.Add; +import org.onebusaway.gtfs.model.Route; import org.opentripplanner.analyst.core.Sample; import org.opentripplanner.analyst.request.SampleFactory; import org.opentripplanner.model.json_serialization.*; @@ -43,8 +42,8 @@ public class AddTripPattern extends Modification { /** used to store the indices of the temporary stops in the graph */ public transient TemporaryStop[] temporaryStops; - /** GTFS mode (route_type), see constants in com.conveyal.gtfs.model.Route */ - public int mode = Route.BUS; + /** GTFS mode (route_type), see constants at https://developers.google.com/transit/gtfs/reference/#routestxt */ + public int mode = 3; /** Create temporary stops associated with the given graph. Note that a given AddTripPattern can be associated only with a single graph. */ public void materialize (Graph graph) { diff --git a/src/main/java/org/opentripplanner/api/adapters/LineStringAdapter.java b/src/main/java/org/opentripplanner/api/adapters/LineStringAdapter.java index e9813405111..55b5f66259b 100644 --- a/src/main/java/org/opentripplanner/api/adapters/LineStringAdapter.java +++ b/src/main/java/org/opentripplanner/api/adapters/LineStringAdapter.java @@ -8,8 +8,8 @@ import org.opentripplanner.util.PolylineEncoder; import org.opentripplanner.util.model.EncodedPolylineBean; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; public class LineStringAdapter extends XmlAdapter{ diff --git a/src/main/java/org/opentripplanner/api/common/RoutingResource.java b/src/main/java/org/opentripplanner/api/common/RoutingResource.java index fe444a2c212..04acf2a34fc 100644 --- a/src/main/java/org/opentripplanner/api/common/RoutingResource.java +++ b/src/main/java/org/opentripplanner/api/common/RoutingResource.java @@ -42,7 +42,7 @@ * * @author abyrd */ -public abstract class RoutingResource { +public abstract class RoutingResource { private static final Logger LOG = LoggerFactory.getLogger(RoutingResource.class); @@ -52,7 +52,7 @@ public abstract class RoutingResource { * a path because we don't want it to be instantiated as an endpoint. Instead, the {routerId} * path parameter should be included in the path annotations of all its subclasses. */ - @PathParam("routerId") + @PathParam("routerId") public String routerId; /** The start location -- either latitude, longitude pair in degrees or a Vertex @@ -72,15 +72,15 @@ public abstract class RoutingResource { /** The date that the trip should depart (or arrive, for requests where arriveBy is true). */ @QueryParam("date") protected String date; - + /** The time that the trip should depart (or arrive, for requests where arriveBy is true). */ @QueryParam("time") protected String time; - + /** Whether the trip should depart or arrive at the specified date and time. */ @QueryParam("arriveBy") protected Boolean arriveBy; - + /** Whether the trip must be wheelchair accessible. */ @QueryParam("wheelchair") protected Boolean wheelchair; @@ -146,19 +146,19 @@ public abstract class RoutingResource { /** For bike triangle routing, how much safety matters (range 0-1). */ @QueryParam("triangleSafetyFactor") protected Double triangleSafetyFactor; - + /** For bike triangle routing, how much slope matters (range 0-1). */ @QueryParam("triangleSlopeFactor") protected Double triangleSlopeFactor; - - /** For bike triangle routing, how much time matters (range 0-1). */ + + /** For bike triangle routing, how much time matters (range 0-1). */ @QueryParam("triangleTimeFactor") protected Double triangleTimeFactor; /** The set of characteristics that the user wants to optimize for. @See OptimizeType */ @QueryParam("optimize") protected OptimizeType optimize; - + /** *

The set of modes that a user is willing to use, with qualifiers stating whether vehicles should be parked, rented, etc.

*

The possible values of the comma-separated list are:

@@ -214,18 +214,18 @@ public abstract class RoutingResource { * to wait for preferred route. */ @QueryParam("otherThanPreferredRoutesPenalty") protected Integer otherThanPreferredRoutesPenalty; - + /** The comma-separated list of preferred agencies. */ @QueryParam("preferredAgencies") protected String preferredAgencies; - + /** * The list of unpreferred routes. The format is agency_[routename][_routeid], so TriMet_100 (100 is route short name) or Trimet__42 (two * underscores, 42 is the route internal ID). */ @QueryParam("unpreferredRoutes") protected String unpreferredRoutes; - + /** The comma-separated list of unpreferred agencies. */ @QueryParam("unpreferredAgencies") protected String unpreferredAgencies; @@ -243,14 +243,14 @@ public abstract class RoutingResource { */ @QueryParam("walkBoardCost") protected Integer walkBoardCost; - + /** * Prevents unnecessary transfers by adding a cost for boarding a vehicle. This is the cost that * is used when boarding while cycling. This is usually higher that walkBoardCost. */ @QueryParam("bikeBoardCost") protected Integer bikeBoardCost; - + /** * The comma-separated list of banned routes. The format is agency_[routename][_routeid], so TriMet_100 (100 is route short name) or Trimet__42 * (two underscores, 42 is the route internal ID). @@ -263,7 +263,7 @@ public abstract class RoutingResource { */ @QueryParam("whiteListedRoutes") protected String whiteListedRoutes; - + /** The comma-separated list of banned agencies. */ @QueryParam("bannedAgencies") protected String bannedAgencies; @@ -273,7 +273,7 @@ public abstract class RoutingResource { */ @QueryParam("whiteListedAgencies") protected String whiteListedAgencies; - + /** The comma-separated list of banned trips. The format is agency_trip[:stop*], so: * TriMet_24601 or TriMet_24601:0:1:2:17:18:19 */ @@ -288,7 +288,7 @@ public abstract class RoutingResource { */ @QueryParam("bannedStops") protected String bannedStops; - + /** A comma-separated list of banned stops. A stop is banned by ignoring its * pre-board and pre-alight edges. This means the stop will be reachable via the * street network. It is not possible to travel through the stop. @@ -298,7 +298,7 @@ public abstract class RoutingResource { */ @QueryParam("bannedStopsHard") protected String bannedStopsHard; - + /** * An additional penalty added to boardings after the first. The value is in OTP's * internal weight units, which are roughly equivalent to seconds. Set this to a high @@ -307,7 +307,7 @@ public abstract class RoutingResource { */ @QueryParam("transferPenalty") protected Integer transferPenalty; - + /** * An additional penalty added to boardings after the first when the transfer is not * preferred. Preferred transfers also include timed transfers. The value is in OTP's @@ -318,7 +318,7 @@ public abstract class RoutingResource { */ @QueryParam("nonpreferredTransferPenalty") protected Integer nonpreferredTransferPenalty; - + /** The maximum number of transfers (that is, one plus the maximum number of boardings) * that a trip will be allowed. Larger values will slow performance, but could give * better routes. This is limited on the server side by the MAX_TRANSFERS value in @@ -361,10 +361,10 @@ public abstract class RoutingResource { */ @QueryParam("reverseOptimizeOnTheFly") protected Boolean reverseOptimizeOnTheFly; - + @QueryParam("boardSlack") private Integer boardSlack; - + @QueryParam("alightSlack") private Integer alightSlack; @@ -384,6 +384,36 @@ public abstract class RoutingResource { @QueryParam("disableRemainingWeightHeuristic") protected Boolean disableRemainingWeightHeuristic; + /* + * Control the size of flag-stop buffer returned in API response. This parameter only applies + * to GTFS-Flex routing, which must be explicitly turned on via the useFlexService parameter in + * router-config.json. + */ + @QueryParam("flexFlagStopBufferSize") + protected Double flexFlagStopBufferSize; + + /** + * Whether to use reservation-based services + */ + @QueryParam("flexUseReservationServices") + protected Boolean flexUseReservationServices = true; + + /** + * Whether to use eligibility-based services + */ + @QueryParam("flexUseEligibilityServices") + protected Boolean flexUseEligibilityServices = true; + + /** + * Whether to ignore DRT time limits. + * + * According to the GTFS-flex spec, demand-response transit (DRT) service must be reserved + * at least `drt_advance_book_min` minutes in advance. OTP not allow DRT service to be used + * inside that time window, unless this parameter is set to true. + */ + @QueryParam("flexIgnoreDrtAdvanceBookMin") + protected Boolean flexIgnoreDrtAdvanceBookMin; + @QueryParam("maxHours") private Double maxHours; @@ -471,8 +501,8 @@ public abstract class RoutingResource { @QueryParam("maximumMicromobilitySpeed") private Double maximumMicromobilitySpeed; - /* - * somewhat ugly bug fix: the graphService is only needed here for fetching per-graph time zones. + /* + * somewhat ugly bug fix: the graphService is only needed here for fetching per-graph time zones. * this should ideally be done when setting the routing context, but at present departure/ * arrival time is stored in the request as an epoch time with the TZ already resolved, and other * code depends on this behavior. (AMB) @@ -526,6 +556,8 @@ protected RoutingRequest buildRequest() throws ParameterException { } else { request.setDateTime(date, time, tz); } + + request.resetClockTime(); } if (wheelchair != null) @@ -626,7 +658,7 @@ protected RoutingRequest buildRequest() throws ParameterException { request.setWhiteListedAgencies(whiteListedAgencies); HashMap bannedTripMap = makeBannedTripMap(bannedTrips); - + if (bannedTripMap != null) request.bannedTrips = bannedTripMap; @@ -635,7 +667,7 @@ protected RoutingRequest buildRequest() throws ParameterException { if (bannedStopsHard != null) request.setBannedStopsHard(bannedStopsHard); - + // The "Least transfers" optimization is accomplished via an increased transfer penalty. // See comment on RoutingRequest.transferPentalty. if (transferPenalty != null) request.transferPenalty = transferPenalty; @@ -675,7 +707,7 @@ protected RoutingRequest buildRequest() throws ParameterException { if (request.boardSlack + request.alightSlack > request.transferSlack) { throw new RuntimeException("Invalid parameters: " + - "transfer slack must be greater than or equal to board slack plus alight slack"); + "transfer slack must be greater than or equal to board slack plus alight slack"); } if (maxTransfers != null) @@ -705,6 +737,18 @@ protected RoutingRequest buildRequest() throws ParameterException { if (disableRemainingWeightHeuristic != null) request.disableRemainingWeightHeuristic = disableRemainingWeightHeuristic; + if (flexFlagStopBufferSize != null) + request.flexFlagStopBufferSize = flexFlagStopBufferSize; + + if (flexUseReservationServices != null) + request.flexUseReservationServices = flexUseReservationServices; + + if (flexUseEligibilityServices != null) + request.flexUseEligibilityServices = flexUseEligibilityServices; + + if (flexIgnoreDrtAdvanceBookMin != null) + request.flexIgnoreDrtAdvanceBookMin = flexIgnoreDrtAdvanceBookMin; + if (maxHours != null) request.maxHours = maxHours; @@ -733,7 +777,7 @@ protected RoutingRequest buildRequest() throws ParameterException { // first TNC before transit. (See StateEditor.boardHailedCar) if (this.modes != null && this.modes.qModes.contains(new QualifiedMode("CAR_HAIL"))) { if (companies == null) { - companies = "NOAPI"; + companies = "NOAPI"; } request.companies = companies; @@ -838,7 +882,7 @@ private HashMap makeBannedTripMap(String banned) { if (banned == null) { return null; } - + HashMap bannedTripMap = new HashMap(); String[] tripStrings = banned.split(","); for (String tripString : tripStrings) { diff --git a/src/main/java/org/opentripplanner/api/model/BoardAlightType.java b/src/main/java/org/opentripplanner/api/model/BoardAlightType.java new file mode 100644 index 00000000000..f1886d44394 --- /dev/null +++ b/src/main/java/org/opentripplanner/api/model/BoardAlightType.java @@ -0,0 +1,27 @@ +package org.opentripplanner.api.model; + +/** + * Distinguish between special ways a passenger may board or alight at a stop. The majority of + * boardings and alightings will be of type "default" -- a regular boarding or alighting at a + * regular transit stop. Currently, the only non-default types are related to GTFS-Flex, but this + * pattern can be extended as necessary. + */ +public enum BoardAlightType { + + /** + * A regular boarding or alighting at a fixed-route transit stop. + */ + DEFAULT, + + /** + * A flag-stop boarding or alighting, e.g. flagging the bus down or a passenger asking the bus + * driver for a drop-off between stops. This is specific to GTFS-Flex. + */ + FLAG_STOP, + + /** + * A boarding or alighting at which the vehicle deviates from its fixed route to drop off a + * passenger. This is specific to GTFS-Flex. + */ + DEVIATED; +} diff --git a/src/main/java/org/opentripplanner/api/model/GeometryAdapter.java b/src/main/java/org/opentripplanner/api/model/GeometryAdapter.java index 463e44d1155..ea5c462072b 100644 --- a/src/main/java/org/opentripplanner/api/model/GeometryAdapter.java +++ b/src/main/java/org/opentripplanner/api/model/GeometryAdapter.java @@ -4,9 +4,9 @@ import org.opentripplanner.common.geometry.GeometryUtils; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.io.WKTReader; -import com.vividsolutions.jts.io.gml2.GMLWriter; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.io.gml2.GMLWriter; public class GeometryAdapter extends XmlAdapter { public Geometry unmarshal(String val) throws Exception { diff --git a/src/main/java/org/opentripplanner/api/model/Itinerary.java b/src/main/java/org/opentripplanner/api/model/Itinerary.java index 523c39fadb4..81c6a65c0a9 100644 --- a/src/main/java/org/opentripplanner/api/model/Itinerary.java +++ b/src/main/java/org/opentripplanner/api/model/Itinerary.java @@ -6,9 +6,6 @@ import java.util.List; import java.util.TimeZone; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; - import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.routing.core.Fare; @@ -81,8 +78,6 @@ public class Itinerary { * trip on a particular vehicle. So a trip where the use walks to the Q train, transfers to the * 6, then walks to their destination, has four legs. */ - @XmlElementWrapper(name = "legs") - @XmlElement(name = "leg") public List legs = new ArrayList(); /** diff --git a/src/main/java/org/opentripplanner/api/model/Leg.java b/src/main/java/org/opentripplanner/api/model/Leg.java index ffc251f9fd9..93d51f6d9aa 100644 --- a/src/main/java/org/opentripplanner/api/model/Leg.java +++ b/src/main/java/org/opentripplanner/api/model/Leg.java @@ -8,16 +8,13 @@ import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.util.model.EncodedPolylineBean; -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.Locale; import java.util.TimeZone; - /** +/** * One leg of a trip -- that is, a temporally continuous piece of the journey that takes place on a * particular vehicle (or on foot). */ @@ -28,12 +25,12 @@ public class Leg { * The date and time this leg begins. */ public Calendar startTime = null; - + /** * The date and time this leg ends. */ public Calendar endTime = null; - + /** * For transit leg, the offset from the scheduled departure-time of the boarding stop in this leg. * "scheduled time of departure at boarding stop" = startTime - departureDelay @@ -48,24 +45,24 @@ public class Leg { * Whether there is real-time data about this Leg */ public Boolean realTime = false; - + /** * Is this a frequency-based trip with non-strict departure times? */ public Boolean isNonExactFrequency = null; - + /** - * The best estimate of the time between two arriving vehicles. This is particularly important - * for non-strict frequency trips, but could become important for real-time trips, strict + * The best estimate of the time between two arriving vehicles. This is particularly important + * for non-strict frequency trips, but could become important for real-time trips, strict * frequency trips, and scheduled trips with empirical headways. */ public Integer headway = null; - + /** * The distance traveled while traversing the leg in meters. */ public Double distance = null; - + /** * Is this leg a traversing pathways? */ @@ -74,7 +71,6 @@ public class Leg { /** * The mode (e.g., Walk) used when traversing this leg. */ - @XmlAttribute @JsonSerialize public String mode = TraverseMode.WALK.toString(); @@ -82,30 +78,24 @@ public class Leg { * For transit legs, the route of the bus or train being used. For non-transit legs, the name of * the street being traversed. */ - @XmlAttribute @JsonSerialize public String route = ""; - @XmlAttribute @JsonSerialize public String agencyName; - @XmlAttribute @JsonSerialize public String agencyUrl; - @XmlAttribute @JsonSerialize public String agencyBrandingUrl; - @XmlAttribute @JsonSerialize public int agencyTimeZoneOffset; /** * For transit leg, the route's (background) color (if one exists). For non-transit legs, null. */ - @XmlAttribute @JsonSerialize public String routeColor = null; @@ -115,10 +105,9 @@ public class Leg { * When equal or highter than 100, it is coded using the Hierarchical Vehicle Type (HVT) codes from the European TPEG standard * Also see http://groups.google.com/group/gtfs-changes/msg/ed917a69cf8c5bef */ - @XmlAttribute @JsonSerialize public Integer routeType = null; - + /** * For transit legs, the ID of the route. * For non-transit legs, null. @@ -128,36 +117,31 @@ public class Leg { /** * For transit leg, the route's text color (if one exists). For non-transit legs, null. */ - @XmlAttribute @JsonSerialize public String routeTextColor = null; /** * For transit legs, if the rider should stay on the vehicle as it changes route names. */ - @XmlAttribute @JsonSerialize public Boolean interlineWithPreviousLeg; - + /** * For transit leg, the trip's short name (if one exists). For non-transit legs, null. */ - @XmlAttribute @JsonSerialize public String tripShortName = null; /** * For transit leg, the trip's block ID (if one exists). For non-transit legs, null. */ - @XmlAttribute @JsonSerialize public String tripBlockId = null; - + /** * For transit legs, the headsign of the bus or train being used. For non-transit legs, null. */ - @XmlAttribute @JsonSerialize public String headsign = null; @@ -165,36 +149,33 @@ public class Leg { * For transit legs, the ID of the transit agency that operates the service used for this leg. * For non-transit legs, null. */ - @XmlAttribute @JsonSerialize public String agencyId = null; - + /** * For transit legs, the ID of the trip. * For non-transit legs, null. */ public FeedScopedId tripId = null; - + /** * For transit legs, the service date of the trip. * For non-transit legs, null. */ - @XmlAttribute @JsonSerialize public String serviceDate = null; - /** - * For transit leg, the route's branding URL (if one exists). For non-transit legs, null. - */ - @XmlAttribute - @JsonSerialize - public String routeBrandingUrl = null; + /** + * For transit leg, the route's branding URL (if one exists). For non-transit legs, null. + */ + @JsonSerialize + public String routeBrandingUrl = null; - /** + /** * The Place where the leg originates. */ public Place from = null; - + /** * The Place where the leg begins. */ @@ -205,7 +186,6 @@ public class Leg { * For non-transit legs, null. * This field is optional i.e. it is always null unless "showIntermediateStops" parameter is set to "true" in the planner request. */ - @XmlElementWrapper(name = "intermediateStops") @JsonProperty(value="intermediateStops") public List stop; @@ -216,53 +196,88 @@ public class Leg { public List interStopGeometry; - /** - * A series of turn by turn instructions used for walking, biking and driving. + /** + * A series of turn by turn instructions used for walking, biking and driving. */ - @XmlElementWrapper(name = "steps") @JsonProperty(value="steps") public List walkSteps; - @XmlElement @JsonSerialize public List alerts; - @XmlAttribute @JsonSerialize public String routeShortName; - @XmlAttribute @JsonSerialize public String routeLongName; - @XmlAttribute @JsonSerialize public String boardRule; - @XmlAttribute @JsonSerialize public String alightRule; - @XmlAttribute @JsonSerialize public Boolean rentedBike; - @XmlAttribute @JsonSerialize public Boolean rentedCar; - @XmlAttribute @JsonSerialize public Boolean rentedVehicle; - @XmlAttribute @JsonSerialize public Boolean hailedCar; - @XmlAttribute @JsonSerialize public TransportationNetworkCompanySummary tncData; + /** + * True if this is a call-and-ride leg. + */ + @JsonSerialize + public Boolean callAndRide; + + /* For call-n-ride leg, supply maximum start time based on calculation. */ + @JsonSerialize + public Calendar flexCallAndRideMaxStartTime = null; + + /* For call-n-ride leg, supply minimum end time based on calculation. */ + @JsonSerialize + public Calendar flexCallAndRideMinEndTime = null; + + /** trip.drt_advance_book_min if this is a demand-response leg */ + @JsonSerialize + public double flexDrtAdvanceBookMin; + + /** + * Agency message if this is leg has a demand-response pickup and the Trip has + * `drt_pickup_message` defined. + */ + @JsonSerialize + public String flexDrtPickupMessage; + + /** + * Agency message if this is leg has a demand-response dropoff and the Trip has + * `drt_drop_off_message` defined. + */ + @JsonSerialize + public String flexDrtDropOffMessage; + + /** + * Agency message if this is leg has a flag stop pickup and the Trip has + * `continuous_pickup_message` defined. + */ + @JsonSerialize + public String flexFlagStopPickupMessage; + + /** + * Agency message if this is leg has a flag stop dropoff and the Trip has + * `continuous_drop_off_message` defined. + */ + @JsonSerialize + public String flexFlagStopDropOffMessage; + /** * Whether this leg is a transit leg or not. * @return Boolean true if the leg is a transit leg @@ -275,11 +290,10 @@ public Boolean isTransitLeg() { else if (mode.equals(TraverseMode.MICROMOBILITY.toString())) return false; else return true; } - - /** + + /** * The leg's duration in seconds */ - @XmlElement @JsonSerialize public double getDuration() { return endTime.getTimeInMillis()/1000.0 - startTime.getTimeInMillis()/1000.0; diff --git a/src/main/java/org/opentripplanner/api/model/Place.java b/src/main/java/org/opentripplanner/api/model/Place.java index 728d0c79e45..36643232d35 100644 --- a/src/main/java/org/opentripplanner/api/model/Place.java +++ b/src/main/java/org/opentripplanner/api/model/Place.java @@ -2,46 +2,45 @@ import java.util.Calendar; import java.util.Set; -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlElement; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.util.Constants; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.opentripplanner.util.model.EncodedPolylineBean; -/** -* A Place is where a journey starts or ends, or a transit stop along the way. -*/ +/** + * A Place is where a journey starts or ends, or a transit stop along the way. + */ public class Place { - /** + /** * For transit stops, the name of the stop. For points of interest, the name of the POI. */ public String name = null; - /** + /** * The ID of the stop. This is often something that users don't care about. */ public FeedScopedId stopId = null; - /** + /** * The "code" of the stop. Depending on the transit agency, this is often * something that users care about. */ public String stopCode = null; /** - * The code or name identifying the quay/platform the vehicle will arrive at or depart from - * - */ + * The code or name identifying the quay/platform the vehicle will arrive at or depart from + * + */ public String platformCode = null; /** * The longitude of the place. */ public Double lon = null; - + /** * The latitude of the place. */ @@ -57,25 +56,21 @@ public class Place { */ public Calendar departure = null; - @XmlAttribute @JsonSerialize public String orig; - @XmlAttribute @JsonSerialize public String zoneId; /** * For transit trips, the stop index (numbered from zero from the start of the trip */ - @XmlAttribute @JsonSerialize public Integer stopIndex; /** * For transit trips, the sequence number of the stop. Per GTFS, these numbers are increasing. */ - @XmlAttribute @JsonSerialize public Integer stopSequence; @@ -83,7 +78,6 @@ public class Place { * Type of vertex. (Normal, Bike sharing station, Bike P+R, Transit stop) * Mostly used for better localization of bike sharing and P+R station names */ - @XmlAttribute @JsonSerialize public VertexType vertexType; @@ -95,20 +89,29 @@ public class Place { /** * Car share station fields */ - @XmlAttribute @JsonSerialize public Set networks; - @XmlAttribute @JsonSerialize public String address; + /** + * This is an optional field which can be used to distinguish among ways a passenger's + * boarding or alighting at a stop can differ among services operated by a transit agency. + * This will be "default" in most cases. Currently the only non-default values are for + * GTFS-Flex board or alight types. + */ + public BoardAlightType boardAlightType; + + /** + * Board or alight area for flag stops + */ + public EncodedPolylineBean flagStopArea; /** * Returns the geometry in GeoJSON format * @return */ - @XmlElement String getGeometry() { return Constants.GEO_JSON_POINT + lon + "," + lat + Constants.GEO_JSON_TAIL; } @@ -120,7 +123,7 @@ public Place(Double lon, Double lat, String name) { this.lon = lon; this.lat = lat; this.name = name; - this.vertexType = VertexType.NORMAL; + this.vertexType = VertexType.NORMAL; } public Place(Double lon, Double lat, String name, Calendar arrival, Calendar departure) { diff --git a/src/main/java/org/opentripplanner/api/model/RouterInfo.java b/src/main/java/org/opentripplanner/api/model/RouterInfo.java index 2d511a1e2c7..2b35e907410 100644 --- a/src/main/java/org/opentripplanner/api/model/RouterInfo.java +++ b/src/main/java/org/opentripplanner/api/model/RouterInfo.java @@ -1,11 +1,17 @@ package org.opentripplanner.api.model; -import com.conveyal.geojson.GeometryDeserializer; -import com.conveyal.geojson.GeometrySerializer; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; + +import org.locationtech.jts.geom.Coordinate; +import org.opentripplanner.common.geometry.GeometryDeserializer; +import org.opentripplanner.common.geometry.GeometrySerializer; import org.opentripplanner.routing.bike_rental.BikeRentalStationService; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.graph.Graph; @@ -13,34 +19,20 @@ import org.opentripplanner.util.TravelOptionsMaker; import org.opentripplanner.util.WorldEnvelope; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; - -@XmlRootElement(name = "RouterInfo") public class RouterInfo { private final BikeRentalStationService service; - @XmlElement public String routerId; - - @JsonSerialize(using=GeometrySerializer.class) - @JsonDeserialize(using=GeometryDeserializer.class) - @XmlJavaTypeAdapter(value=GeometryAdapter.class,type=Geometry.class) + + @JsonSerialize(using= GeometrySerializer.class) + @JsonDeserialize(using= GeometryDeserializer.class) public Geometry polygon; - @XmlElement public Date buildTime; - @XmlElement public long transitServiceStarts; - @XmlElement public long transitServiceEnds; public HashSet transitModes; diff --git a/src/main/java/org/opentripplanner/api/model/RouterList.java b/src/main/java/org/opentripplanner/api/model/RouterList.java index d1bed4c2de2..613533aefd3 100644 --- a/src/main/java/org/opentripplanner/api/model/RouterList.java +++ b/src/main/java/org/opentripplanner/api/model/RouterList.java @@ -3,13 +3,6 @@ import java.util.ArrayList; import java.util.List; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElements; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement(name = "RouterList") public class RouterList { - @XmlElements(value = { @XmlElement(name="routerInfo") }) public List routerInfo = new ArrayList(); - } diff --git a/src/main/java/org/opentripplanner/api/model/TripPlan.java b/src/main/java/org/opentripplanner/api/model/TripPlan.java index 7c116cc945e..52d707bb98b 100644 --- a/src/main/java/org/opentripplanner/api/model/TripPlan.java +++ b/src/main/java/org/opentripplanner/api/model/TripPlan.java @@ -4,8 +4,6 @@ import java.util.Date; import java.util.List; -import javax.xml.bind.annotation.XmlElementWrapper; - import com.fasterxml.jackson.annotation.JsonProperty; /** @@ -23,7 +21,6 @@ public class TripPlan { public Place to = null; /** A list of possible itineraries */ - @XmlElementWrapper(name="itineraries") //TODO: why don't we just change the variable name? @JsonProperty(value="itineraries") public List itinerary = new ArrayList(); diff --git a/src/main/java/org/opentripplanner/api/model/WalkStep.java b/src/main/java/org/opentripplanner/api/model/WalkStep.java index ad0ed31e30f..056b576b46d 100644 --- a/src/main/java/org/opentripplanner/api/model/WalkStep.java +++ b/src/main/java/org/opentripplanner/api/model/WalkStep.java @@ -2,15 +2,13 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.collect.Lists; + import org.opentripplanner.api.model.alertpatch.LocalizedAlert; import org.opentripplanner.common.model.P2; import org.opentripplanner.profile.BikeRentalStationInfo; import org.opentripplanner.routing.alertpatch.Alert; import org.opentripplanner.routing.graph.Edge; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlTransient; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -100,10 +98,8 @@ public class WalkStep { * The elevation profile as a comma-separated list of x,y values. x is the distance from the start of the step, y is the elevation at this * distance. */ - @XmlTransient public List> elevation; - @XmlElement @JsonSerialize public List alerts; @@ -141,7 +137,7 @@ public String toString() { } public static RelativeDirection getRelativeDirection(double lastAngle, double thisAngle, - boolean roundabout) { + boolean roundabout) { double angleDiff = thisAngle - lastAngle; if (angleDiff < 0) { @@ -206,7 +202,6 @@ public String streetNameNoParens() { return streetName.substring(0, idx - 1); } - @XmlJavaTypeAdapter(ElevationAdapter.class) @JsonSerialize public List> getElevation() { return elevation; diff --git a/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchCreationResponse.java b/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchCreationResponse.java index 026c46cf01c..14f6a08c173 100644 --- a/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchCreationResponse.java +++ b/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchCreationResponse.java @@ -1,10 +1,5 @@ package org.opentripplanner.api.model.alertpatch; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement(name = "AlertPatchCreationResponse") public class AlertPatchCreationResponse { - @XmlElement public String status; } diff --git a/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchResponse.java b/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchResponse.java index add91d63e7e..bb6c6a59fd9 100644 --- a/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchResponse.java +++ b/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchResponse.java @@ -3,19 +3,9 @@ import java.util.ArrayList; import java.util.List; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlElements; -import javax.xml.bind.annotation.XmlRootElement; - import org.opentripplanner.routing.alertpatch.AlertPatch; -@XmlRootElement public class AlertPatchResponse { - @XmlElementWrapper - @XmlElements({ - @XmlElement(name = "AlertPatch", type = AlertPatch.class) - }) public List alertPatches; public void addAlertPatch(AlertPatch alertPatch) { diff --git a/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchSet.java b/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchSet.java index 7fe797de775..20a4d79da86 100644 --- a/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchSet.java +++ b/src/main/java/org/opentripplanner/api/model/alertpatch/AlertPatchSet.java @@ -2,16 +2,8 @@ import java.util.List; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElements; -import javax.xml.bind.annotation.XmlRootElement; - import org.opentripplanner.routing.alertpatch.AlertPatch; -@XmlRootElement(name="AlertPatchSet") public class AlertPatchSet { - @XmlElements({ - @XmlElement(name = "AlertPatch", type = AlertPatch.class) - }) public List alertPatches; } diff --git a/src/main/java/org/opentripplanner/api/model/alertpatch/LocalizedAlert.java b/src/main/java/org/opentripplanner/api/model/alertpatch/LocalizedAlert.java index 3a3a9f23a4d..94ffdc2b491 100644 --- a/src/main/java/org/opentripplanner/api/model/alertpatch/LocalizedAlert.java +++ b/src/main/java/org/opentripplanner/api/model/alertpatch/LocalizedAlert.java @@ -4,21 +4,14 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.opentripplanner.routing.alertpatch.Alert; -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlTransient; import java.util.Date; import java.util.Locale; -@XmlRootElement(name = "Alert") public class LocalizedAlert { - @XmlTransient @JsonIgnore public Alert alert; - @XmlTransient @JsonIgnore private Locale locale; @@ -30,7 +23,6 @@ public LocalizedAlert(Alert alert, Locale locale) { public LocalizedAlert(){ } - @XmlAttribute @JsonSerialize public String getAlertHeaderText() { if (alert.alertHeaderText == null) { @@ -39,7 +31,6 @@ public String getAlertHeaderText() { return alert.alertHeaderText.toString(locale); } - @XmlAttribute @JsonSerialize public String getAlertDescriptionText() { if (alert.alertDescriptionText == null) { @@ -48,7 +39,6 @@ public String getAlertDescriptionText() { return alert.alertDescriptionText.toString(locale); } - @XmlAttribute @JsonSerialize public String getAlertUrl() { if (alert.alertUrl == null) { @@ -58,7 +48,6 @@ public String getAlertUrl() { } //null means unknown - @XmlElement @JsonSerialize public Date getEffectiveStartDate() { return alert.effectiveStartDate; diff --git a/src/main/java/org/opentripplanner/api/model/error/TransitError.java b/src/main/java/org/opentripplanner/api/model/error/TransitError.java index 5c5bb3b96b8..450fefa9a94 100644 --- a/src/main/java/org/opentripplanner/api/model/error/TransitError.java +++ b/src/main/java/org/opentripplanner/api/model/error/TransitError.java @@ -1,9 +1,5 @@ package org.opentripplanner.api.model.error; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement public class TransitError { private String message; @@ -17,7 +13,6 @@ public void setMessage(String message) { this.message = message; } - @XmlElement(name="message") public String getMessage() { return message; } diff --git a/src/main/java/org/opentripplanner/api/parameter/BoundingBox.java b/src/main/java/org/opentripplanner/api/parameter/BoundingBox.java index 1bfa0f70837..17299cfeb54 100644 --- a/src/main/java/org/opentripplanner/api/parameter/BoundingBox.java +++ b/src/main/java/org/opentripplanner/api/parameter/BoundingBox.java @@ -4,8 +4,8 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; public class BoundingBox { diff --git a/src/main/java/org/opentripplanner/api/parameter/BoundingCircle.java b/src/main/java/org/opentripplanner/api/parameter/BoundingCircle.java index 8ba579a17a7..cd15b98d5cd 100644 --- a/src/main/java/org/opentripplanner/api/parameter/BoundingCircle.java +++ b/src/main/java/org/opentripplanner/api/parameter/BoundingCircle.java @@ -4,8 +4,8 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; public class BoundingCircle { diff --git a/src/main/java/org/opentripplanner/api/resource/BikeRental.java b/src/main/java/org/opentripplanner/api/resource/BikeRental.java index 41efc1fd8bd..1f76ede65af 100644 --- a/src/main/java/org/opentripplanner/api/resource/BikeRental.java +++ b/src/main/java/org/opentripplanner/api/resource/BikeRental.java @@ -20,7 +20,7 @@ import org.opentripplanner.standalone.OTPServer; import org.opentripplanner.standalone.Router; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.util.ResourceBundleSingleton; @Path("/routers/{routerId}/bike_rental") diff --git a/src/main/java/org/opentripplanner/api/resource/BikeRentalStationList.java b/src/main/java/org/opentripplanner/api/resource/BikeRentalStationList.java index 7d9431075a2..9a28ef89599 100644 --- a/src/main/java/org/opentripplanner/api/resource/BikeRentalStationList.java +++ b/src/main/java/org/opentripplanner/api/resource/BikeRentalStationList.java @@ -3,14 +3,8 @@ import java.util.ArrayList; import java.util.List; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElements; -import javax.xml.bind.annotation.XmlRootElement; - import org.opentripplanner.routing.bike_rental.BikeRentalStation; -@XmlRootElement(name="BikeRentalStationList") public class BikeRentalStationList { - @XmlElements(value = { @XmlElement(name="station") }) public List stations = new ArrayList(); } diff --git a/src/main/java/org/opentripplanner/api/resource/CarRental.java b/src/main/java/org/opentripplanner/api/resource/CarRental.java index 953579e7913..6378211fcdb 100644 --- a/src/main/java/org/opentripplanner/api/resource/CarRental.java +++ b/src/main/java/org/opentripplanner/api/resource/CarRental.java @@ -13,7 +13,7 @@ the License, or (at your option) any later version. package org.opentripplanner.api.resource; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.routing.car_rental.CarRentalStation; import org.opentripplanner.routing.car_rental.CarRentalStationService; import org.opentripplanner.standalone.OTPServer; diff --git a/src/main/java/org/opentripplanner/api/resource/CoordinateArrayListSequence.java b/src/main/java/org/opentripplanner/api/resource/CoordinateArrayListSequence.java index bb4f7626e98..3ee8a6b47a7 100644 --- a/src/main/java/org/opentripplanner/api/resource/CoordinateArrayListSequence.java +++ b/src/main/java/org/opentripplanner/api/resource/CoordinateArrayListSequence.java @@ -3,9 +3,9 @@ import java.util.ArrayList; import java.util.Arrays; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Envelope; /** An instance of CoordinateSequence that can be efficiently extended */ public class CoordinateArrayListSequence implements CoordinateSequence, Cloneable { @@ -124,4 +124,17 @@ public void add(Coordinate newCoordinate) { public void clear() { coordinates = new ArrayList(); } + + @Override + public CoordinateSequence copy() { + CoordinateArrayListSequence clone; + try { + clone = (CoordinateArrayListSequence) super.clone(); + } catch (CloneNotSupportedException e) { + /* never happens since super is Object */ + throw new RuntimeException(e); + } + clone.coordinates = (ArrayList) coordinates.clone(); + return clone; + } } diff --git a/src/main/java/org/opentripplanner/api/resource/ExternalGeocoderResource.java b/src/main/java/org/opentripplanner/api/resource/ExternalGeocoderResource.java index b3effa58466..859bc41a7b8 100644 --- a/src/main/java/org/opentripplanner/api/resource/ExternalGeocoderResource.java +++ b/src/main/java/org/opentripplanner/api/resource/ExternalGeocoderResource.java @@ -13,7 +13,7 @@ import org.opentripplanner.geocoder.Geocoder; import org.opentripplanner.geocoder.GeocoderResults; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; /** * Maybe the internal geocoder resource should just chain to defined external geocoders? diff --git a/src/main/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverter.java b/src/main/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverter.java index c298f6a3e87..f42825a595e 100644 --- a/src/main/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverter.java +++ b/src/main/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverter.java @@ -1,8 +1,9 @@ package org.opentripplanner.api.resource; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.api.model.BoardAlightType; import org.opentripplanner.api.model.Itinerary; import org.opentripplanner.api.model.Leg; import org.opentripplanner.api.model.Place; @@ -43,6 +44,8 @@ import org.opentripplanner.routing.edgetype.StreetEdge; import org.opentripplanner.routing.edgetype.TripPattern; import org.opentripplanner.routing.error.TransportationNetworkCompanyAvailabilityException; +import org.opentripplanner.routing.edgetype.flex.PartialPatternHop; +import org.opentripplanner.routing.edgetype.flex.TemporaryDirectPatternHop; import org.opentripplanner.routing.error.TrivialPathException; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; @@ -252,9 +255,13 @@ public static Itinerary generateItinerary(GraphPath path, boolean showIntermedia private static Calendar makeCalendar(State state) { RoutingContext rctx = state.getContext(); - TimeZone timeZone = rctx.graph.getTimeZone(); + TimeZone timeZone = rctx.graph.getTimeZone(); + return makeCalendar(timeZone, state.getTimeInMillis()); + } + + private static Calendar makeCalendar(TimeZone timeZone, long timeMillis) { Calendar calendar = Calendar.getInstance(timeZone); - calendar.setTimeInMillis(state.getTimeInMillis()); + calendar.setTimeInMillis(timeMillis); return calendar; } @@ -264,11 +271,11 @@ private static Calendar makeCalendar(State state) { * @param edges The array of input edges * @return The coordinates of the points on the edges */ - private static CoordinateArrayListSequence makeCoordinates(Edge[] edges) { + public static CoordinateArrayListSequence makeCoordinates(Edge[] edges) { CoordinateArrayListSequence coordinates = new CoordinateArrayListSequence(); for (Edge edge : edges) { - LineString geometry = edge.getGeometry(); + LineString geometry = edge.getDisplayGeometry(); if (geometry != null) { if (coordinates.size() == 0) { @@ -461,7 +468,7 @@ private static boolean addTNCData( // This avoids unnecessary/redundant API requests to TNC providers. Place from = leg.from; if (request.transportationNetworkCompanyEtaAtOrigin > -1 && - (i == 0 || (i == 1 && itinerary.legs.get(0).mode.equals("WALK")))) { + (i == 0 || (i == 1 && itinerary.legs.get(0).mode.equals("WALK")))) { from = new Place(request.from.lng, request.from.lat, request.from.name); tncLegsAreFromOrigin.add(true); } else { @@ -687,18 +694,18 @@ private static void calculateTimes(Itinerary itinerary, State[] states) { if (state.getBackMode() == null) continue; switch (state.getBackMode()) { - default: - itinerary.transitTime += state.getTimeDeltaSeconds(); - break; - - case LEG_SWITCH: - itinerary.waitingTime += state.getTimeDeltaSeconds(); - break; - - case WALK: - case BICYCLE: - case CAR: - itinerary.walkTime += state.getTimeDeltaSeconds(); + default: + itinerary.transitTime += state.getTimeDeltaSeconds(); + break; + + case LEG_SWITCH: + itinerary.waitingTime += state.getTimeDeltaSeconds(); + break; + + case WALK: + case BICYCLE: + case CAR: + itinerary.walkTime += state.getTimeDeltaSeconds(); } } } @@ -801,6 +808,11 @@ private static void addTripFields(Leg leg, State[] states, Locale requestedLocal leg.tripId = trip.getId(); leg.tripShortName = trip.getTripShortName(); leg.tripBlockId = trip.getBlockId(); + leg.flexDrtAdvanceBookMin = trip.getDrtAdvanceBookMin(); + leg.flexDrtPickupMessage = trip.getDrtPickupMessage(); + leg.flexDrtDropOffMessage = trip.getDrtDropOffMessage(); + leg.flexFlagStopPickupMessage = trip.getContinuousPickupMessage(); + leg.flexFlagStopDropOffMessage = trip.getContinuousDropOffMessage(); if (serviceDay != null) { leg.serviceDate = serviceDay.getServiceDate().getAsString(); @@ -809,6 +821,30 @@ private static void addTripFields(Leg leg, State[] states, Locale requestedLocal if (leg.headsign == null) { leg.headsign = trip.getTripHeadsign(); } + + Edge edge = states[states.length - 1].backEdge; + if (edge instanceof TemporaryDirectPatternHop) { + leg.callAndRide = true; + } + if (edge instanceof PartialPatternHop) { + PartialPatternHop hop = (PartialPatternHop) edge; + int directTime = hop.getDirectVehicleTime(); + TripTimes tt = states[states.length - 1].getTripTimes(); + int maxTime = tt.getDemandResponseMaxTime(directTime); + int avgTime = tt.getDemandResponseAvgTime(directTime); + int delta = maxTime - avgTime; + if (directTime != 0 && delta > 0) { + if (hop.isDeviatedRouteBoard()) { + long maxStartTime = leg.startTime.getTimeInMillis() + (delta * 1000); + leg.flexCallAndRideMaxStartTime = makeCalendar(leg.startTime.getTimeZone(), maxStartTime); + } + if (hop.isDeviatedRouteAlight()) { + long minEndTime = leg.endTime.getTimeInMillis() - (delta * 1000); + leg.flexCallAndRideMinEndTime = makeCalendar(leg.endTime.getTimeZone(), minEndTime); + } + } + } + } } @@ -828,9 +864,9 @@ private static void addPlaces(Leg leg, State[] states, Edge[] edges, boolean sho Vertex lastVertex = states[states.length - 1].getVertex(); Stop firstStop = firstVertex instanceof TransitVertex ? - ((TransitVertex) firstVertex).getStop(): null; + ((TransitVertex) firstVertex).getStop(): null; Stop lastStop = lastVertex instanceof TransitVertex ? - ((TransitVertex) lastVertex).getStop(): null; + ((TransitVertex) lastVertex).getStop(): null; TripTimes tripTimes = states[states.length - 1].getTripTimes(); leg.from = makePlace(states[0], firstVertex, edges[0], firstStop, tripTimes, requestedLocale); @@ -887,7 +923,7 @@ private static Place makePlace(State state, Vertex vertex, Edge edge, Stop stop, name = ((StreetVertex) vertex).getIntersectionName(requestedLocale).toString(requestedLocale); } Place place = new Place(vertex.getX(), vertex.getY(), name, - makeCalendar(state), makeCalendar(state)); + makeCalendar(state), makeCalendar(state)); if (endOfLeg) edge = state.getBackEdge(); @@ -904,6 +940,22 @@ private static Place makePlace(State state, Vertex vertex, Edge edge, Stop stop, place.stopSequence = tripTimes.getStopSequence(place.stopIndex); } place.vertexType = VertexType.TRANSIT; + place.boardAlightType = BoardAlightType.DEFAULT; + if (edge instanceof PartialPatternHop) { + PartialPatternHop hop = (PartialPatternHop) edge; + if (hop.hasBoardArea() && !endOfLeg) { + place.flagStopArea = PolylineEncoder.createEncodings(hop.getBoardArea()); + } + if (hop.hasAlightArea() && endOfLeg) { + place.flagStopArea = PolylineEncoder.createEncodings(hop.getAlightArea()); + } + if ((endOfLeg && hop.isFlagStopAlight()) || (!endOfLeg && hop.isFlagStopBoard())) { + place.boardAlightType = BoardAlightType.FLAG_STOP; + } + if ((endOfLeg && hop.isDeviatedRouteAlight()) || (!endOfLeg && hop.isDeviatedRouteBoard())) { + place.boardAlightType = BoardAlightType.DEVIATED; + } + } } else if(vertex instanceof BikeRentalStationVertex) { place.bikeShareId = ((BikeRentalStationVertex) vertex).getId(); LOG.trace("Added bike share Id {} to place", place.bikeShareId); @@ -945,9 +997,9 @@ private static void addRealTimeData(Leg leg, State[] states) { /** * Converts a list of street edges to a list of turn-by-turn directions. - * + * * @param previous a non-transit leg that immediately precedes this one (bike-walking, say), or null - * + * * @return */ public static List generateWalkSteps(Graph graph, State[] states, WalkStep previous, Locale requestedLocale) { @@ -1049,9 +1101,9 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk // new step, set distance to length of first edge distance = edge.getDistance(); } else if (((step.streetName != null && !step.streetNameNoParens().equals(streetNameNoParens)) - && (!step.bogusName || !edge.hasBogusName())) || - edge.isRoundabout() != (roundaboutExit > 0) || // went on to or off of a roundabout - isLink(edge) && !isLink(backState.getBackEdge())) { + && (!step.bogusName || !edge.hasBogusName())) || + edge.isRoundabout() != (roundaboutExit > 0) || // went on to or off of a roundabout + isLink(edge) && !isLink(backState.getBackEdge())) { // Street name has changed, or we've gone on to or off of a roundabout. if (roundaboutExit > 0) { // if we were just on a roundabout, @@ -1084,7 +1136,7 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk /* street name has not changed */ double thisAngle = DirectionUtils.getFirstAngle(geom); RelativeDirection direction = WalkStep.getRelativeDirection(lastAngle, thisAngle, - edge.isRoundabout()); + edge.isRoundabout()); boolean optionsBefore = backState.multipleOptionsBefore(); if (edge.isRoundabout()) { // we are on a roundabout, and have already traversed at least one edge of it. @@ -1114,7 +1166,7 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk continue; } double altAngle = DirectionUtils.getFirstAngle(alternative - .getGeometry()); + .getGeometry()); double altAngleDiff = getAbsoluteAngleDiff(altAngle, lastAngle); if (angleDiff > Math.PI / 4 || altAngleDiff - angleDiff < Math.PI / 16) { shouldGenerateContinue = true; @@ -1128,7 +1180,7 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk Vertex backVertex = twoStatesBack.getVertex(); for (Edge alternative : backVertex.getOutgoingStreetEdges()) { List alternatives = alternative.getToVertex() - .getOutgoingStreetEdges(); + .getOutgoingStreetEdges(); if (alternatives.size() == 0) { continue; // this is not an alternative } @@ -1139,7 +1191,7 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk continue; } double altAngle = DirectionUtils.getFirstAngle(alternative - .getGeometry()); + .getGeometry()); double altAngleDiff = getAbsoluteAngleDiff(altAngle, lastAngle); if (angleDiff > Math.PI / 4 || altAngleDiff - angleDiff < Math.PI / 16) { shouldGenerateContinue = true; @@ -1180,30 +1232,30 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk WalkStep lastStep = steps.get(last); if (twoBack.distance < MAX_ZAG_DISTANCE - && lastStep.streetNameNoParens().equals(threeBack.streetNameNoParens())) { - - if (((lastStep.relativeDirection == RelativeDirection.RIGHT || - lastStep.relativeDirection == RelativeDirection.HARD_RIGHT) && - (twoBack.relativeDirection == RelativeDirection.RIGHT || + && lastStep.streetNameNoParens().equals(threeBack.streetNameNoParens())) { + + if (((lastStep.relativeDirection == RelativeDirection.RIGHT || + lastStep.relativeDirection == RelativeDirection.HARD_RIGHT) && + (twoBack.relativeDirection == RelativeDirection.RIGHT || twoBack.relativeDirection == RelativeDirection.HARD_RIGHT)) || - ((lastStep.relativeDirection == RelativeDirection.LEFT || + ((lastStep.relativeDirection == RelativeDirection.LEFT || lastStep.relativeDirection == RelativeDirection.HARD_LEFT) && (twoBack.relativeDirection == RelativeDirection.LEFT || - twoBack.relativeDirection == RelativeDirection.HARD_LEFT))) { + twoBack.relativeDirection == RelativeDirection.HARD_LEFT))) { // in this case, we have two left turns or two right turns in quick // succession; this is probably a U-turn. - + steps.remove(last - 1); - + lastStep.distance += twoBack.distance; - + // A U-turn to the left, typical in the US. - if (lastStep.relativeDirection == RelativeDirection.LEFT || - lastStep.relativeDirection == RelativeDirection.HARD_LEFT) + if (lastStep.relativeDirection == RelativeDirection.LEFT || + lastStep.relativeDirection == RelativeDirection.HARD_LEFT) lastStep.relativeDirection = RelativeDirection.UTURN_LEFT; else lastStep.relativeDirection = RelativeDirection.UTURN_RIGHT; - + // in this case, we're definitely staying on the same street // (since it's zag removal, the street names are the same) lastStep.stayOn = true; @@ -1232,7 +1284,7 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk } else { if (!createdNewStep && step.elevation != null) { List> s = encodeElevationProfile(edge, distance, - backState.getOptions().geoidElevation ? -graph.ellipsoidToGeoidDifference : 0); + backState.getOptions().geoidElevation ? -graph.ellipsoidToGeoidDifference : 0); if (step.elevation != null && step.elevation.size() > 0) { step.elevation.addAll(s); } else { @@ -1253,12 +1305,12 @@ public static List generateWalkSteps(Graph graph, State[] states, Walk // add bike rental information if applicable if(onBikeRentalState != null && !steps.isEmpty()) { - steps.get(steps.size()-1).bikeRentalOnStation = - new BikeRentalStationInfo((BikeRentalStationVertex) onBikeRentalState.getBackEdge().getToVertex()); + steps.get(steps.size()-1).bikeRentalOnStation = + new BikeRentalStationInfo((BikeRentalStationVertex) onBikeRentalState.getBackEdge().getToVertex()); } if(offBikeRentalState != null && !steps.isEmpty()) { - steps.get(0).bikeRentalOffStation = - new BikeRentalStationInfo((BikeRentalStationVertex) offBikeRentalState.getBackEdge().getFromVertex()); + steps.get(0).bikeRentalOffStation = + new BikeRentalStationInfo((BikeRentalStationVertex) offBikeRentalState.getBackEdge().getFromVertex()); } return steps; @@ -1288,7 +1340,7 @@ private static WalkStep createWalkStep(Graph graph, State s, Locale wantedLocale step.lon = en.getFromVertex().getX(); step.lat = en.getFromVertex().getY(); step.elevation = encodeElevationProfile(s.getBackEdge(), 0, - s.getOptions().geoidElevation ? -graph.ellipsoidToGeoidDifference : 0); + s.getOptions().geoidElevation ? -graph.ellipsoidToGeoidDifference : 0); step.bogusName = en.hasBogusName(); step.addAlerts(graph.streetNotesService.getNotes(s), wantedLocale); step.angle = DirectionUtils.getFirstAngle(s.getBackEdge().getGeometry()); diff --git a/src/main/java/org/opentripplanner/api/resource/InspectorLayersList.java b/src/main/java/org/opentripplanner/api/resource/InspectorLayersList.java index b5ba3f269cf..4f95f30a3ca 100644 --- a/src/main/java/org/opentripplanner/api/resource/InspectorLayersList.java +++ b/src/main/java/org/opentripplanner/api/resource/InspectorLayersList.java @@ -4,21 +4,15 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElements; -import javax.xml.bind.annotation.XmlRootElement; + import org.opentripplanner.inspector.TileRenderer; /** * * @author mabu */ -@XmlRootElement(name="InspectorLayersList") public class InspectorLayersList { - @XmlElements(value = {@XmlElement(name="layer") }) public List layers; InspectorLayersList(Map renderers) { @@ -32,10 +26,8 @@ public class InspectorLayersList { private static class InspectorLayer { - @XmlAttribute @JsonSerialize String key; - @XmlAttribute @JsonSerialize String name; diff --git a/src/main/java/org/opentripplanner/api/resource/LIsochrone.java b/src/main/java/org/opentripplanner/api/resource/LIsochrone.java index dc2f975d1ae..05e4e875df7 100644 --- a/src/main/java/org/opentripplanner/api/resource/LIsochrone.java +++ b/src/main/java/org/opentripplanner/api/resource/LIsochrone.java @@ -40,7 +40,7 @@ import org.slf4j.LoggerFactory; import com.google.common.io.Files; -import com.vividsolutions.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.MultiPolygon; /** * Return isochrone geometry as a set of GeoJSON or zipped-shapefile multi-polygons. diff --git a/src/main/java/org/opentripplanner/api/resource/ParkAndRide.java b/src/main/java/org/opentripplanner/api/resource/ParkAndRide.java index 2188e709191..3254be8c5ac 100644 --- a/src/main/java/org/opentripplanner/api/resource/ParkAndRide.java +++ b/src/main/java/org/opentripplanner/api/resource/ParkAndRide.java @@ -13,8 +13,8 @@ the License, or (at your option) any later version. package org.opentripplanner.api.resource; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.vertextype.ParkAndRideVertex; import org.opentripplanner.routing.vertextype.TransitStop; diff --git a/src/main/java/org/opentripplanner/api/resource/Response.java b/src/main/java/org/opentripplanner/api/resource/Response.java index b7415059ce8..c11810bf222 100644 --- a/src/main/java/org/opentripplanner/api/resource/Response.java +++ b/src/main/java/org/opentripplanner/api/resource/Response.java @@ -5,18 +5,14 @@ import java.util.Map.Entry; import javax.ws.rs.core.UriInfo; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; import org.opentripplanner.api.model.TripPlan; import org.opentripplanner.api.model.error.PlannerError; /** Represents a trip planner response, will be serialized into XML or JSON by Jersey */ -@XmlRootElement public class Response { /** A dictionary of the parameters provided in the request that triggered this response. */ - @XmlElement public HashMap requestParameters; private TripPlan plan; private PlannerError error = null; @@ -57,7 +53,6 @@ public void setPlan(TripPlan plan) { } /** The error (if any) that this response raised. */ - @XmlElement(required=false) public PlannerError getError() { return error; } diff --git a/src/main/java/org/opentripplanner/api/resource/Routers.java b/src/main/java/org/opentripplanner/api/resource/Routers.java index caac36b0b6a..22ff5249550 100644 --- a/src/main/java/org/opentripplanner/api/resource/Routers.java +++ b/src/main/java/org/opentripplanner/api/resource/Routers.java @@ -32,7 +32,6 @@ import org.opentripplanner.graph_builder.GraphBuilder; import org.opentripplanner.routing.error.GraphNotFoundException; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.graph.Graph.LoadLevel; import org.opentripplanner.routing.impl.MemoryGraphSource; import org.opentripplanner.routing.services.GraphService; import org.opentripplanner.standalone.CommandLineParameters; @@ -46,40 +45,40 @@ /** * This REST API endpoint allows remotely loading, reloading, and evicting graphs on a running server. - * + * * A GraphService maintains a mapping between routerIds and specific Graph objects. * The HTTP verbs are used as follows to manipulate that mapping: - * + * * GET - see the registered routerIds and Graphs, verify whether a particular routerId is registered * PUT - create or replace a mapping from a routerId to a Graph loaded from the server filesystem * POST - create or replace a mapping from a routerId to a serialized Graph sent in the request * DELETE - de-register a routerId, releasing the reference to the associated graph - * + * * The HTTP request URLs are of the form /ws/routers/{routerId}, where the routerId is optional. * If a routerId is supplied in the URL, the verb will act upon the mapping for that specific * routerId. If no routerId is given, the verb will act upon all routerIds currently registered. - * + * * For example: - * + * * GET http://localhost/otp-rest-servlet/ws/routers * will retrieve a list of all registered routerId -> Graph mappings and their geographic bounds. - * + * * GET http://localhost/otp-rest-servlet/ws/routers/london * will return status code 200 and a brief description of the 'london' graph including geographic * bounds, or 404 if the 'london' routerId is not registered. - * + * * PUT http://localhost/otp-rest-servlet/ws/routers * will reload the graphs for all currently registered routerIds from disk. - * + * * PUT http://localhost/otp-rest-servlet/ws/routers/paris * will load a Graph from a sub-directory called 'paris' and associate it with the routerId 'paris'. - * + * * DELETE http://localhost/otp-rest-servlet/ws/routers/paris * will release the Paris Graph and de-register the 'paris' routerId. - * + * * DELETE http://localhost/otp-rest-servlet/ws/routers * will de-register all currently registered routerIds. - * + * * The GET methods are not secured, but all other methods are secured under ROLE_ROUTERS. * See documentation for individual methods for additional parameters. */ @@ -91,7 +90,7 @@ public class Routers { @Context OTPServer otpServer; - /** + /** * Returns a list of routers and their bounds. * @return a representation of the graphs and their geographic bounds, in JSON or XML depending * on the Accept header in the HTTP request. @@ -110,7 +109,7 @@ public RouterList getRouterIds() { return routerList; } - /** + /** * Returns the bounds for a specific routerId, or verifies whether it is registered. * @returns status code 200 if the routerId is registered, otherwise a 404. */ @@ -121,11 +120,11 @@ public RouterInfo getGraphId(@PathParam("routerId") String routerId) { RouterInfo routerInfo = getRouterInfo(routerId); if (routerInfo == null) throw new WebApplicationException(Response.status(Status.NOT_FOUND) - .entity("Graph id '" + routerId + "' not registered.\n").type("text/plain") - .build()); + .entity("Graph id '" + routerId + "' not registered.\n").type("text/plain") + .build()); return routerInfo; } - + private RouterInfo getRouterInfo(String routerId) { try { Router router = otpServer.getRouter(routerId); @@ -138,19 +137,19 @@ private RouterInfo getRouterInfo(String routerId) { } } - /** + /** * Reload the graphs for all registered routerIds from disk. */ @RolesAllowed({ "ROUTERS" }) @PUT @Produces({ MediaType.APPLICATION_JSON }) public Response reloadGraphs(@QueryParam("path") String path, - @QueryParam("preEvict") @DefaultValue("true") boolean preEvict, - @QueryParam("force") @DefaultValue("true") boolean force) { + @QueryParam("preEvict") @DefaultValue("true") boolean preEvict, + @QueryParam("force") @DefaultValue("true") boolean force) { otpServer.getGraphService().reloadGraphs(preEvict, force); return Response.status(Status.OK).build(); } - /** + /** * Load the graph for the specified routerId from disk. * @param preEvict before reloading each graph, evict the existing graph. This will prevent * memory usage from increasing during the reload, but routing will be unavailable on this @@ -159,7 +158,7 @@ public Response reloadGraphs(@QueryParam("path") String path, @RolesAllowed({ "ROUTERS" }) @PUT @Path("{routerId}") @Produces({ MediaType.TEXT_PLAIN }) public Response putGraphId(@PathParam("routerId") String routerId, - @QueryParam("preEvict") @DefaultValue("true") boolean preEvict) { + @QueryParam("preEvict") @DefaultValue("true") boolean preEvict) { LOG.debug("Attempting to load graph '{}' from server's local filesystem.", routerId); GraphService graphService = otpServer.getGraphService(); if (graphService.getRouterIds().contains(routerId)) { @@ -170,7 +169,7 @@ public Response putGraphId(@PathParam("routerId") String routerId, return Response.status(404).entity("graph already registered, but reload failed.\n").build(); } else { boolean success = graphService.registerGraph(routerId, graphService - .getGraphSourceFactory().createGraphSource(routerId)); + .getGraphSourceFactory().createGraphSource(routerId)); if (success) return Response.status(201).entity("graph registered.\n").build(); else @@ -178,7 +177,7 @@ public Response putGraphId(@PathParam("routerId") String routerId, } } - /** + /** * Deserialize a graph sent with the HTTP request as POST data, associating it with the given * routerId. */ @@ -186,10 +185,9 @@ public Response putGraphId(@PathParam("routerId") String routerId, @POST @Path("{routerId}") @Produces({ MediaType.TEXT_PLAIN }) @Consumes(MediaType.APPLICATION_OCTET_STREAM) public Response postGraphOverWire ( - @PathParam("routerId") String routerId, - @QueryParam("preEvict") @DefaultValue("true") boolean preEvict, - @QueryParam("loadLevel") @DefaultValue("FULL") LoadLevel level, - InputStream is) { + @PathParam("routerId") String routerId, + @QueryParam("preEvict") @DefaultValue("true") boolean preEvict, + InputStream is) { if (preEvict) { LOG.debug("pre-evicting graph"); otpServer.getGraphService().evictRouter(routerId); @@ -197,7 +195,7 @@ public Response postGraphOverWire ( LOG.debug("deserializing graph from POST data stream..."); Graph graph; try { - graph = Graph.load(is, level); + graph = Graph.load(is); GraphService graphService = otpServer.getGraphService(); graphService.registerGraph(routerId, new MemoryGraphSource(routerId, graph)); return Response.status(Status.CREATED).entity(graph.toString() + "\n").build(); @@ -205,7 +203,7 @@ public Response postGraphOverWire ( return Response.status(Status.BAD_REQUEST).entity(e.toString() + "\n").build(); } } - + /** * Build a graph from data in the ZIP file posted over the wire, associating it with the given router ID. * This method will be selected when the Content-Type is application/zip. @@ -214,35 +212,35 @@ public Response postGraphOverWire ( @POST @Path("{routerId}") @Consumes({"application/zip"}) @Produces({ MediaType.TEXT_PLAIN }) public Response buildGraphOverWire ( - @PathParam("routerId") String routerId, - @QueryParam("preEvict") @DefaultValue("true") boolean preEvict, - InputStream input) { + @PathParam("routerId") String routerId, + @QueryParam("preEvict") @DefaultValue("true") boolean preEvict, + InputStream input) { // TODO: async processing - + if (preEvict) { LOG.debug("Pre-evicting graph with routerId {} before building new graph", routerId); otpServer.getGraphService().evictRouter(routerId); } - + // get a temporary directory, using Google Guava File tempDir = Files.createTempDir(); - + // extract the zip file to the temp dir ZipInputStream zis = new ZipInputStream(input); - + try { for (ZipEntry entry = zis.getNextEntry(); entry != null; entry = zis.getNextEntry()) { if (entry.isDirectory()) // we only support flat ZIP files return Response.status(Response.Status.BAD_REQUEST) - .entity("ZIP files containing directories are not supported").build(); - + .entity("ZIP files containing directories are not supported").build(); + File file = new File(tempDir, entry.getName()); - + if (!file.getParentFile().equals(tempDir)) return Response.status(Response.Status.BAD_REQUEST) - .entity("ZIP files containing directories are not supported").build(); - + .entity("ZIP files containing directories are not supported").build(); + OutputStream os = new FileOutputStream(file); ByteStreams.copy(zis, os); os.close(); @@ -256,32 +254,32 @@ public Response buildGraphOverWire ( CommandLineParameters params = otpServer.params.clone(); params.build = tempDir; params.inMemory = true; - + GraphBuilder graphBuilder = GraphBuilder.forDirectory(params, tempDir); - + graphBuilder.run(); - + // remove the temporary directory // this doesn't work for nested directories, but the extract doesn't either, // so we'll crash long before we get here . . . for (File file : tempDir.listFiles()) { file.delete(); } - + tempDir.delete(); - + Graph graph = graphBuilder.getGraph(); // re-index the graph to ensure all data is added and recreate a new streetIndex. It's ok to recreate the // streetIndex because the previous one created during graph build is not needed anymore and isn't able to be // used outside of the graphBuilder. graph.index(true); - + GraphService graphService = otpServer.getGraphService(); graphService.registerGraph(routerId, new MemoryGraphSource(routerId, graph)); return Response.status(Status.CREATED).entity(graph.toString() + "\n").build(); } - - /** + + /** * Save the graph data, but don't load it in memory. The file location is based on routerId. * If the graph already exists, the graph will be overwritten. */ @@ -289,8 +287,8 @@ public Response buildGraphOverWire ( @POST @Path("/save") @Produces({ MediaType.TEXT_PLAIN }) @Consumes(MediaType.APPLICATION_OCTET_STREAM) public Response saveGraphOverWire ( - @QueryParam("routerId") String routerId, - InputStream is) { + @QueryParam("routerId") String routerId, + InputStream is) { LOG.debug("save graph from POST data stream..."); try { boolean success = otpServer.getGraphService().getGraphSourceFactory().save(routerId, is); @@ -313,7 +311,7 @@ public Response deleteAll() { return Response.status(200).entity(message).build(); } - /** + /** * De-register a specific routerId, evicting the associated graph from memory. * @return status code 200 if the routerId was de-registered, * 404 if the routerId was not registered. diff --git a/src/main/java/org/opentripplanner/api/resource/SIsochrone.java b/src/main/java/org/opentripplanner/api/resource/SIsochrone.java index 2bee1913d62..8b313d409dc 100644 --- a/src/main/java/org/opentripplanner/api/resource/SIsochrone.java +++ b/src/main/java/org/opentripplanner/api/resource/SIsochrone.java @@ -1,8 +1,8 @@ package org.opentripplanner.api.resource; import com.google.common.collect.Maps; -import com.vividsolutions.jts.geom.*; -import com.vividsolutions.jts.linearref.LengthIndexedLine; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.linearref.LengthIndexedLine; import org.geotools.geojson.geom.GeometryJSON; import org.geotools.referencing.GeodeticCalculator; import org.opensphere.geometry.algorithm.ConcaveHull; diff --git a/src/main/java/org/opentripplanner/api/resource/ServerInfo.java b/src/main/java/org/opentripplanner/api/resource/ServerInfo.java index 585cbf30db5..b724cf3b2d0 100644 --- a/src/main/java/org/opentripplanner/api/resource/ServerInfo.java +++ b/src/main/java/org/opentripplanner/api/resource/ServerInfo.java @@ -6,8 +6,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; + import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStream; @@ -15,7 +14,6 @@ import java.nio.charset.Charset; @Path("/") -@XmlRootElement public class ServerInfo { /** Quality value prioritizes MIME types */ @@ -29,16 +27,12 @@ public static ServerInfo getServerInfo() { return SERVER_INFO; } - // Fields must be public or have a public getter to be auto-serialized to JSON; - // they are annotated with @XmlElement to be serialized to XML elements (as opposed to attributes). + // Fields must be public or have a public getter to be auto-serialized to JSON - @XmlElement - public MavenVersion serverVersion = MavenVersion.VERSION; + public MavenVersion serverVersion = MavenVersion.VERSION; - @XmlElement public String cpuName = "unknown"; - @XmlElement public int nCores = 0; /* It would make sense to have one object containing maven, git, and hardware subobjects. */ diff --git a/src/main/java/org/opentripplanner/api/resource/SimpleIsochrone.java b/src/main/java/org/opentripplanner/api/resource/SimpleIsochrone.java index 0ab55063d5a..b4e81d05d86 100644 --- a/src/main/java/org/opentripplanner/api/resource/SimpleIsochrone.java +++ b/src/main/java/org/opentripplanner/api/resource/SimpleIsochrone.java @@ -56,11 +56,11 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.common.io.Files; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Point; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; /** * This class provides a vector isochrone generator for places without good OSM street connectivity, diff --git a/src/main/java/org/opentripplanner/api/resource/VehicleRental.java b/src/main/java/org/opentripplanner/api/resource/VehicleRental.java index 52001831af4..07763dfd128 100644 --- a/src/main/java/org/opentripplanner/api/resource/VehicleRental.java +++ b/src/main/java/org/opentripplanner/api/resource/VehicleRental.java @@ -13,7 +13,7 @@ the License, or (at your option) any later version. package org.opentripplanner.api.resource; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.routing.vehicle_rental.VehicleRentalStation; import org.opentripplanner.routing.vehicle_rental.VehicleRentalStationService; import org.opentripplanner.standalone.OTPServer; diff --git a/src/main/java/org/opentripplanner/common/StreetUtils.java b/src/main/java/org/opentripplanner/common/StreetUtils.java index 88195698870..34abb5c318c 100644 --- a/src/main/java/org/opentripplanner/common/StreetUtils.java +++ b/src/main/java/org/opentripplanner/common/StreetUtils.java @@ -1,7 +1,7 @@ package org.opentripplanner.common; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Polygon; import org.opentripplanner.common.geometry.Subgraph; import org.opentripplanner.graph_builder.annotation.GraphConnectivity; import org.opentripplanner.routing.core.RoutingRequest; diff --git a/src/main/java/org/opentripplanner/common/geometry/AccumulativeGridSampler.java b/src/main/java/org/opentripplanner/common/geometry/AccumulativeGridSampler.java index 6da767168d6..172bf5fea2d 100644 --- a/src/main/java/org/opentripplanner/common/geometry/AccumulativeGridSampler.java +++ b/src/main/java/org/opentripplanner/common/geometry/AccumulativeGridSampler.java @@ -7,7 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * Helper class to fill-in a ZSampleGrid from a given loosely-defined set of sampling points. diff --git a/src/main/java/org/opentripplanner/common/geometry/CompactElevationProfile.java b/src/main/java/org/opentripplanner/common/geometry/CompactElevationProfile.java index e5e4e2f0bbc..8aa5704de22 100644 --- a/src/main/java/org/opentripplanner/common/geometry/CompactElevationProfile.java +++ b/src/main/java/org/opentripplanner/common/geometry/CompactElevationProfile.java @@ -2,8 +2,8 @@ import java.io.Serializable; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; /** * Compact elevation profile. To optimize storage, we use the following tricks: diff --git a/src/main/java/org/opentripplanner/common/geometry/CompactLineString.java b/src/main/java/org/opentripplanner/common/geometry/CompactLineString.java index cf7db0291cb..2d2135fc958 100644 --- a/src/main/java/org/opentripplanner/common/geometry/CompactLineString.java +++ b/src/main/java/org/opentripplanner/common/geometry/CompactLineString.java @@ -2,9 +2,9 @@ import java.io.Serializable; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; /** * Compact line string. To optimize storage, we use the following tricks: diff --git a/src/main/java/org/opentripplanner/common/geometry/DelaunayIsolineBuilder.java b/src/main/java/org/opentripplanner/common/geometry/DelaunayIsolineBuilder.java index d5f458dc703..82bf58f0a8f 100644 --- a/src/main/java/org/opentripplanner/common/geometry/DelaunayIsolineBuilder.java +++ b/src/main/java/org/opentripplanner/common/geometry/DelaunayIsolineBuilder.java @@ -10,12 +10,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.algorithm.CGAlgorithms; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.algorithm.CGAlgorithms; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; /** * Compute isoline based on a Delaunay triangulation of z samplings. diff --git a/src/main/java/org/opentripplanner/common/geometry/DelaunayTriangulation.java b/src/main/java/org/opentripplanner/common/geometry/DelaunayTriangulation.java index 8b2e62046b4..d0f6f3cb42e 100644 --- a/src/main/java/org/opentripplanner/common/geometry/DelaunayTriangulation.java +++ b/src/main/java/org/opentripplanner/common/geometry/DelaunayTriangulation.java @@ -1,6 +1,6 @@ package org.opentripplanner.common.geometry; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * A DelaunayPoint is the geometrical point of a node of the triangulation. diff --git a/src/main/java/org/opentripplanner/common/geometry/DirectionUtils.java b/src/main/java/org/opentripplanner/common/geometry/DirectionUtils.java index d920beb2d23..bed0607a214 100644 --- a/src/main/java/org/opentripplanner/common/geometry/DirectionUtils.java +++ b/src/main/java/org/opentripplanner/common/geometry/DirectionUtils.java @@ -2,10 +2,10 @@ import org.apache.commons.math3.util.FastMath; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.MultiLineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; public class DirectionUtils { diff --git a/src/main/java/org/opentripplanner/common/geometry/GeoJsonModule.java b/src/main/java/org/opentripplanner/common/geometry/GeoJsonModule.java new file mode 100644 index 00000000000..c53f6cb5b7b --- /dev/null +++ b/src/main/java/org/opentripplanner/common/geometry/GeoJsonModule.java @@ -0,0 +1,15 @@ +package org.opentripplanner.common.geometry; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.locationtech.jts.geom.Geometry; + +public class GeoJsonModule extends SimpleModule { + + public GeoJsonModule() { + super("GeoJson", new Version(1, 0, 0, null, "com.bedatadriven", "jackson-geojson")); + addSerializer(Geometry.class, new GeometrySerializer()); + addDeserializer(Geometry.class, new GeometryDeserializer()); + } + +} diff --git a/src/main/java/org/opentripplanner/common/geometry/GeometryDeserializer.java b/src/main/java/org/opentripplanner/common/geometry/GeometryDeserializer.java new file mode 100644 index 00000000000..f6fde5fea85 --- /dev/null +++ b/src/main/java/org/opentripplanner/common/geometry/GeometryDeserializer.java @@ -0,0 +1,107 @@ +package org.opentripplanner.common.geometry; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; + +import java.io.IOException; + +public class GeometryDeserializer extends JsonDeserializer { + + private GeometryFactory gf = new GeometryFactory(); + + @Override + public Geometry deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + + ObjectCodec oc = jp.getCodec(); + JsonNode root = oc.readTree(jp); + return parseGeometry(root); + } + + private Geometry parseGeometry(JsonNode root) { + String typeName = root.get("type").asText(); + if (typeName.equals("Point")) { + return gf.createPoint(parseCoordinate(root.get("coordinates"))); + } else if(typeName.equals("MultiPoint")) { + return gf.createMultiPoint(parseLineString(root.get("coordinates"))); + } else if(typeName.equals("LineString")) { + return gf.createLineString(parseLineString(root.get("coordinates"))); + } else if (typeName.equals("MultiLineString")) { + return gf.createMultiLineString(parseLineStrings(root.get("coordinates"))); + } else if(typeName.equals("Polygon")) { + JsonNode arrayOfRings = root.get("coordinates"); + return parsePolygonCoordinates(arrayOfRings); + } else if (typeName.equals("MultiPolygon")) { + JsonNode arrayOfPolygons = root.get("coordinates"); + return gf.createMultiPolygon(parsePolygons(arrayOfPolygons)); + } else if (typeName.equals("GeometryCollection")) { + return gf.createGeometryCollection(parseGeometries(root.get("geometries"))); + } else { + throw new UnsupportedOperationException(); + } + } + + private Geometry[] parseGeometries(JsonNode arrayOfGeoms) { + Geometry[] items = new Geometry[arrayOfGeoms.size()]; + for(int i=0;i!=arrayOfGeoms.size();++i) { + items[i] = parseGeometry(arrayOfGeoms.get(i)); + } + return items; + } + + private Polygon parsePolygonCoordinates(JsonNode arrayOfRings) { + return gf.createPolygon(parseExteriorRing(arrayOfRings), + parseInteriorRings(arrayOfRings)); + } + + private Polygon[] parsePolygons(JsonNode arrayOfPolygons) { + Polygon[] polygons = new Polygon[arrayOfPolygons.size()]; + for (int i = 0; i != arrayOfPolygons.size(); ++i) { + polygons[i] = parsePolygonCoordinates(arrayOfPolygons.get(i)); + } + return polygons; + } + + private LinearRing parseExteriorRing(JsonNode arrayOfRings) { + return gf.createLinearRing(parseLineString(arrayOfRings.get(0))); + } + + private LinearRing[] parseInteriorRings(JsonNode arrayOfRings) { + LinearRing rings[] = new LinearRing[arrayOfRings.size() - 1]; + for (int i = 1; i < arrayOfRings.size(); ++i) { + rings[i - 1] = gf.createLinearRing(parseLineString(arrayOfRings + .get(i))); + } + return rings; + } + + private Coordinate parseCoordinate(JsonNode array) { + return new Coordinate(array.get(0).asDouble(), array.get(1).asDouble()); + } + + private Coordinate[] parseLineString(JsonNode array) { + Coordinate[] points = new Coordinate[array.size()]; + for (int i = 0; i != array.size(); ++i) { + points[i] = parseCoordinate(array.get(i)); + } + return points; + } + + private LineString[] parseLineStrings(JsonNode array) { + LineString[] strings = new LineString[array.size()]; + for (int i = 0; i != array.size(); ++i) { + strings[i] = gf.createLineString(parseLineString(array.get(i))); + } + return strings; + } + +} diff --git a/src/main/java/org/opentripplanner/common/geometry/GeometrySerializer.java b/src/main/java/org/opentripplanner/common/geometry/GeometrySerializer.java new file mode 100644 index 00000000000..dc1e8fba2b4 --- /dev/null +++ b/src/main/java/org/opentripplanner/common/geometry/GeometrySerializer.java @@ -0,0 +1,176 @@ +package org.opentripplanner.common.geometry; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +import java.io.IOException; + +public class GeometrySerializer extends JsonSerializer { + + @Override + public void serialize(Geometry value, JsonGenerator jgen, + SerializerProvider provider) throws IOException, + JsonProcessingException { + + writeGeometry(jgen, value); + } + + public void writeGeometry(JsonGenerator jgen, Geometry value) + throws JsonGenerationException, IOException { + if (value instanceof Polygon) { + writePolygon(jgen, (Polygon) value); + + } else if(value instanceof Point) { + writePoint(jgen, (Point) value); + + } else if (value instanceof MultiPoint) { + writeMultiPoint(jgen, (MultiPoint) value); + + } else if (value instanceof MultiPolygon) { + writeMultiPolygon(jgen, (MultiPolygon) value); + + } else if (value instanceof LineString) { + writeLineString(jgen, (LineString) value); + + } else if (value instanceof MultiLineString) { + writeMultiLineString(jgen, (MultiLineString) value); + } else if (value instanceof GeometryCollection) { + writeGeometryCollection(jgen, (GeometryCollection) value); + + } else { + throw new UnsupportedOperationException("not implemented: " + + value.getClass().getName()); + } + } + + private void writeGeometryCollection(JsonGenerator jgen, + GeometryCollection value) throws JsonGenerationException, + IOException { + jgen.writeStartObject(); + jgen.writeStringField("type", "GeometryCollection"); + jgen.writeArrayFieldStart("geometries"); + + for (int i = 0; i != value.getNumGeometries(); ++i) { + writeGeometry(jgen, value.getGeometryN(i)); + } + + jgen.writeEndArray(); + jgen.writeEndObject(); + } + + private void writeMultiPoint(JsonGenerator jgen, MultiPoint value) + throws JsonGenerationException, IOException { + jgen.writeStartObject(); + jgen.writeStringField("type", "MultiPoint"); + jgen.writeArrayFieldStart("coordinates"); + + for (int i = 0; i != value.getNumGeometries(); ++i) { + writePointCoords(jgen, (Point) value.getGeometryN(i)); + } + + jgen.writeEndArray(); + jgen.writeEndObject(); + } + + private void writeMultiLineString(JsonGenerator jgen, MultiLineString value) + throws JsonGenerationException, IOException { + jgen.writeStartObject(); + jgen.writeStringField("type", "MultiLineString"); + jgen.writeArrayFieldStart("coordinates"); + + for (int i = 0; i != value.getNumGeometries(); ++i) { + writeLineStringCoords(jgen, (LineString) value.getGeometryN(i)); + } + + jgen.writeEndArray(); + jgen.writeEndObject(); + } + + @Override + public Class handledType() { + return Geometry.class; + } + + private void writeMultiPolygon(JsonGenerator jgen, MultiPolygon value) + throws JsonGenerationException, IOException { + jgen.writeStartObject(); + jgen.writeStringField("type", "MultiPolygon"); + jgen.writeArrayFieldStart("coordinates"); + + for (int i = 0; i != value.getNumGeometries(); ++i) { + writePolygonCoordinates(jgen, (Polygon) value.getGeometryN(i)); + } + + jgen.writeEndArray(); + jgen.writeEndObject(); + } + + private void writePolygon(JsonGenerator jgen, Polygon value) + throws JsonGenerationException, IOException { + jgen.writeStartObject(); + jgen.writeStringField("type", "Polygon"); + jgen.writeFieldName("coordinates"); + writePolygonCoordinates(jgen, value); + + jgen.writeEndObject(); + } + + private void writePolygonCoordinates(JsonGenerator jgen, Polygon value) + throws IOException, JsonGenerationException { + jgen.writeStartArray(); + writeLineStringCoords(jgen, value.getExteriorRing()); + + for (int i = 0; i != value.getNumInteriorRing(); ++i) { + writeLineStringCoords(jgen, value.getInteriorRingN(i)); + } + jgen.writeEndArray(); + } + + private void writeLineStringCoords(JsonGenerator jgen, LineString ring) + throws JsonGenerationException, IOException { + jgen.writeStartArray(); + for (int i = 0; i != ring.getNumPoints(); ++i) { + Point p = ring.getPointN(i); + writePointCoords(jgen, p); + } + jgen.writeEndArray(); + } + + private void writeLineString(JsonGenerator jgen, LineString lineString) + throws JsonGenerationException, IOException { + jgen.writeStartObject(); + jgen.writeStringField("type", "LineString"); + jgen.writeFieldName("coordinates"); + writeLineStringCoords(jgen, lineString); + jgen.writeEndObject(); + } + + private void writePoint(JsonGenerator jgen, Point p) + throws JsonGenerationException, IOException { + jgen.writeStartObject(); + jgen.writeStringField("type", "Point"); + jgen.writeFieldName("coordinates"); + writePointCoords(jgen, p); + jgen.writeEndObject(); + } + + private void writePointCoords(JsonGenerator jgen, Point p) + throws IOException, JsonGenerationException { + jgen.writeStartArray(); + jgen.writeNumber(p.getX()); + jgen.writeNumber(p.getY()); + jgen.writeEndArray(); + } + +} diff --git a/src/main/java/org/opentripplanner/common/geometry/GeometryUtils.java b/src/main/java/org/opentripplanner/common/geometry/GeometryUtils.java index cf94279fc41..22a84245699 100644 --- a/src/main/java/org/opentripplanner/common/geometry/GeometryUtils.java +++ b/src/main/java/org/opentripplanner/common/geometry/GeometryUtils.java @@ -1,9 +1,11 @@ package org.opentripplanner.common.geometry; -import com.vividsolutions.jts.geom.*; -import com.vividsolutions.jts.linearref.LengthLocationMap; -import com.vividsolutions.jts.linearref.LinearLocation; -import com.vividsolutions.jts.linearref.LocationIndexedLine; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.linearref.LengthLocationMap; +import org.locationtech.jts.linearref.LinearLocation; +import org.locationtech.jts.linearref.LocationIndexedLine; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; import org.geojson.GeoJsonObject; import org.geojson.LngLatAlt; import org.geotools.referencing.CRS; @@ -98,7 +100,7 @@ public static LineString getInteriorSegment(Geometry geomerty, Coordinate first, } /** - * Adapted from com.vividsolutions.jts.geom.LineSegment + * Adapted from org.locationtech.jts.geom.LineSegment * Combines segmentFraction and projectionFactor methods. */ public static double segmentFraction(double x0, double y0, double x1, double y1, @@ -181,4 +183,13 @@ private static Coordinate[] convertPath(List path) { } return coords; } + + public static Geometry parseWkt(String wkt) { + try { + return new WKTReader(GeometryUtils.getGeometryFactory()).read(wkt); + } catch(ParseException e) { + LOG.error("Unable to parse wkt: " + e); + } + return null; + } } diff --git a/src/main/java/org/opentripplanner/common/geometry/GraphUtils.java b/src/main/java/org/opentripplanner/common/geometry/GraphUtils.java index 4767e9247cb..b5fb95b82cd 100644 --- a/src/main/java/org/opentripplanner/common/geometry/GraphUtils.java +++ b/src/main/java/org/opentripplanner/common/geometry/GraphUtils.java @@ -6,11 +6,11 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.algorithm.ConvexHull; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryCollection; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.simplify.DouglasPeuckerSimplifier; +import org.locationtech.jts.algorithm.ConvexHull; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; public class GraphUtils { diff --git a/src/main/java/org/opentripplanner/common/geometry/HashGridSpatialIndex.java b/src/main/java/org/opentripplanner/common/geometry/HashGridSpatialIndex.java index 6f41b6ac25a..e958737a31b 100644 --- a/src/main/java/org/opentripplanner/common/geometry/HashGridSpatialIndex.java +++ b/src/main/java/org/opentripplanner/common/geometry/HashGridSpatialIndex.java @@ -16,11 +16,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.index.ItemVisitor; -import com.vividsolutions.jts.index.SpatialIndex; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.index.ItemVisitor; +import org.locationtech.jts.index.SpatialIndex; /** * A spatial index using a 2D fast long hashtable (Trove lib). diff --git a/src/main/java/org/opentripplanner/common/geometry/IntPackedCoordinateSequence.java b/src/main/java/org/opentripplanner/common/geometry/IntPackedCoordinateSequence.java deleted file mode 100644 index b5d826ef957..00000000000 --- a/src/main/java/org/opentripplanner/common/geometry/IntPackedCoordinateSequence.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.opentripplanner.common.geometry; - -import java.io.Serializable; - -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.Envelope; - -/** - * 2D, supports only coordinates in range +/- 180. - */ -public class IntPackedCoordinateSequence implements CoordinateSequence, Cloneable, Serializable { - - private static final long serialVersionUID = 1L; - - int[] ordinates; - - private static final double TO_FIXED = Integer.MAX_VALUE / 180.0; - private static final double FROM_FIXED = 180.0 / Integer.MAX_VALUE; - - private int toFixedInt(double latlon) { - return (int) (latlon * TO_FIXED); - } - - private double fromFixedInt(int fixed) { - return (double) (fixed * FROM_FIXED); - } - - public IntPackedCoordinateSequence(Coordinate[] coordinates) { - int nc = coordinates.length; - ordinates = new int[nc * 2]; - int i = 0; - for (Coordinate c : coordinates) { - setOrdinate(i, 0, c.x); - setOrdinate(i, 1, c.y); - i++; - } - } - - @Override - public int getDimension() { - return 2; - } - - @Override - public Coordinate getCoordinate(int i) { - return getCoordinateCopy(i); - } - - @Override - public Coordinate getCoordinateCopy(int i) { - return new Coordinate(getX(i), getY(i)); - } - - @Override - public void getCoordinate(int index, Coordinate coord) { - coord.x = getX(index); - coord.y = getY(index); - } - - @Override - public double getX(int index) { - return fromFixedInt(ordinates[2*index]); - } - - @Override - public double getY(int index) { - return fromFixedInt(ordinates[2*index + 1]); - } - - @Override - public double getOrdinate(int index, int ordinateIndex) { - return fromFixedInt(ordinates[2*index + ordinateIndex]); - } - - @Override - public int size() { - return ordinates.length / 2; - } - - @Override - public void setOrdinate(int index, int ordinateIndex, double value) { - ordinates[2*index + ordinateIndex] = toFixedInt(value); - } - - @Override - public Coordinate[] toCoordinateArray() { - Coordinate[] ret = new Coordinate[this.size()]; - for (int i = 0; i < this.size(); i++) { - ret[i] = this.getCoordinate(i); - } - return ret; - } - - @Override - public Envelope expandEnvelope(Envelope env) { - for (int i = 0; i < ordinates.length / 2; i++) { - env.expandToInclude(getX(i), getY(i)); - } - return env; - } - - @Override - public IntPackedCoordinateSequence clone() { - try { - return (IntPackedCoordinateSequence) super.clone(); - } catch (CloneNotSupportedException e) { - throw new RuntimeException("programming error.", e); - } - } -} diff --git a/src/main/java/org/opentripplanner/common/geometry/IsolineBuilder.java b/src/main/java/org/opentripplanner/common/geometry/IsolineBuilder.java index 82d8079d84d..b4195b7b7ac 100644 --- a/src/main/java/org/opentripplanner/common/geometry/IsolineBuilder.java +++ b/src/main/java/org/opentripplanner/common/geometry/IsolineBuilder.java @@ -1,6 +1,6 @@ package org.opentripplanner.common.geometry; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; /** * Generic interface for a class that compute an isoline out of a TZ 2D "field". diff --git a/src/main/java/org/opentripplanner/common/geometry/PackedCoordinateSequence.java b/src/main/java/org/opentripplanner/common/geometry/PackedCoordinateSequence.java index 50246d92d10..c22d120c4cd 100644 --- a/src/main/java/org/opentripplanner/common/geometry/PackedCoordinateSequence.java +++ b/src/main/java/org/opentripplanner/common/geometry/PackedCoordinateSequence.java @@ -3,9 +3,9 @@ import java.io.Serializable; import java.lang.ref.SoftReference; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Envelope; /** * A {@link CoordinateSequence} implementation based on a packed arrays. In this implementation, @@ -36,14 +36,14 @@ public abstract class PackedCoordinateSequence implements CoordinateSequence, Se transient protected SoftReference coordRef; /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getDimension() + * @see org.locationtech.jts.geom.CoordinateSequence#getDimension() */ public int getDimension() { return this.dimension; } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getCoordinate(int) + * @see org.locationtech.jts.geom.CoordinateSequence#getCoordinate(int) */ public Coordinate getCoordinate(int i) { Coordinate[] coords = getCachedCoords(); @@ -54,14 +54,14 @@ public Coordinate getCoordinate(int i) { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getCoordinate(int) + * @see org.locationtech.jts.geom.CoordinateSequence#getCoordinate(int) */ public Coordinate getCoordinateCopy(int i) { return getCoordinateInternal(i); } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getCoordinate(int) + * @see org.locationtech.jts.geom.CoordinateSequence#getCoordinate(int) */ public void getCoordinate(int i, Coordinate coord) { coord.x = getOrdinate(i, 0); @@ -69,7 +69,7 @@ public void getCoordinate(int i, Coordinate coord) { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#toCoordinateArray() + * @see org.locationtech.jts.geom.CoordinateSequence#toCoordinateArray() */ public Coordinate[] toCoordinateArray() { Coordinate[] coords = getCachedCoords(); @@ -107,21 +107,21 @@ private Coordinate[] getCachedCoords() { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getX(int) + * @see org.locationtech.jts.geom.CoordinateSequence#getX(int) */ public double getX(int index) { return getOrdinate(index, 0); } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getY(int) + * @see org.locationtech.jts.geom.CoordinateSequence#getY(int) */ public double getY(int index) { return getOrdinate(index, 1); } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getOrdinate(int, int) + * @see org.locationtech.jts.geom.CoordinateSequence#getOrdinate(int, int) */ public abstract double getOrdinate(int index, int ordinateIndex); @@ -261,8 +261,6 @@ public Double(Coordinate[] coordinates) { /** * Builds a new empty packed coordinate sequence of a given size and dimension - * - * @param coordinates */ public Double(int size, int dimension) { this.dimension = dimension; @@ -270,7 +268,7 @@ public Double(int size, int dimension) { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getCoordinate(int) + * @see org.locationtech.jts.geom.CoordinateSequence#getCoordinate(int) */ public Coordinate getCoordinateInternal(int i) { double x = coords[i * dimension]; @@ -280,7 +278,7 @@ public Coordinate getCoordinateInternal(int i) { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#size() + * @see org.locationtech.jts.geom.CoordinateSequence#size() */ public int size() { return coords.length / dimension; @@ -296,7 +294,7 @@ public Object clone() { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getOrdinate(int, int) Beware, for + * @see org.locationtech.jts.geom.CoordinateSequence#getOrdinate(int, int) Beware, for * performace reasons the ordinate index is not checked, if it's over dimensions you * may not get an exception but a meaningless value. */ @@ -305,7 +303,7 @@ public double getOrdinate(int index, int ordinate) { } /** - * @see com.vividsolutions.jts.geom.PackedCoordinateSequence#setOrdinate(int, int, double) + * @see org.locationtech.jts.geom.CoordinateSequence#setOrdinate(int, int, double) */ public void setOrdinate(int index, int ordinate, double value) { coordRef = null; @@ -318,6 +316,13 @@ public Envelope expandEnvelope(Envelope env) { } return env; } + + @Override + public CoordinateSequence copy() { + Double clone = (Double) super.clone(); + clone.coords = coords.clone(); + return clone; + } } /** @@ -352,9 +357,6 @@ public Float(float[] coords, int dimensions) { /** * Constructs a packed coordinate sequence from an array of doubles - * - * @param coordinates - * @param dimension */ public Float(double[] coordinates, int dimensions) { this.coords = new float[coordinates.length]; @@ -386,8 +388,6 @@ public Float(Coordinate[] coordinates, int dimension) { /** * Constructs an empty packed coordinate sequence of a given size and dimension - * - * @param coordinates */ public Float(int size, int dimension) { this.dimension = dimension; @@ -395,7 +395,7 @@ public Float(int size, int dimension) { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getCoordinate(int) + * @see org.locationtech.jts.geom.CoordinateSequence#getCoordinate(int) */ public Coordinate getCoordinateInternal(int i) { double x = coords[i * dimension]; @@ -405,7 +405,7 @@ public Coordinate getCoordinateInternal(int i) { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#size() + * @see org.locationtech.jts.geom.CoordinateSequence#size() */ public int size() { return coords.length / dimension; @@ -421,7 +421,7 @@ public Object clone() { } /** - * @see com.vividsolutions.jts.geom.CoordinateSequence#getOrdinate(int, int) Beware, for + * @see org.locationtech.jts.geom.CoordinateSequence#getOrdinate(int, int) Beware, for * performace reasons the ordinate index is not checked, if it's over dimensions you * may not get an exception but a meaningless value. */ @@ -430,7 +430,7 @@ public double getOrdinate(int index, int ordinate) { } /** - * @see com.vividsolutions.jts.geom.PackedCoordinateSequence#setOrdinate(int, int, double) + * @see org.locationtech.jts.geom.CoordinateSequence#setOrdinate(int, int, double) */ public void setOrdinate(int index, int ordinate, double value) { coordRef = null; @@ -444,6 +444,12 @@ public Envelope expandEnvelope(Envelope env) { return env; } + @Override + public CoordinateSequence copy() { + Float clone = (Float) super.clone(); + clone.coords = coords.clone(); + return clone; + } } public String toString() { diff --git a/src/main/java/org/opentripplanner/common/geometry/RecursiveGridIsolineBuilder.java b/src/main/java/org/opentripplanner/common/geometry/RecursiveGridIsolineBuilder.java index e6a09d16b3f..d1b4483ec42 100644 --- a/src/main/java/org/opentripplanner/common/geometry/RecursiveGridIsolineBuilder.java +++ b/src/main/java/org/opentripplanner/common/geometry/RecursiveGridIsolineBuilder.java @@ -15,12 +15,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.algorithm.CGAlgorithms; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.algorithm.CGAlgorithms; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; /** * Compute isoline based on a zFunc and a set of initial coverage points P0={(x,y)} to seed the diff --git a/src/main/java/org/opentripplanner/common/geometry/ReversibleLineStringWrapper.java b/src/main/java/org/opentripplanner/common/geometry/ReversibleLineStringWrapper.java index 04a9011d64a..94420a95a57 100644 --- a/src/main/java/org/opentripplanner/common/geometry/ReversibleLineStringWrapper.java +++ b/src/main/java/org/opentripplanner/common/geometry/ReversibleLineStringWrapper.java @@ -1,7 +1,7 @@ package org.opentripplanner.common.geometry; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.LineString; public class ReversibleLineStringWrapper { diff --git a/src/main/java/org/opentripplanner/common/geometry/Serializable2DPackedCoordinateSequenceFactory.java b/src/main/java/org/opentripplanner/common/geometry/Serializable2DPackedCoordinateSequenceFactory.java index 0aaab78fa85..9efd40c3b47 100644 --- a/src/main/java/org/opentripplanner/common/geometry/Serializable2DPackedCoordinateSequenceFactory.java +++ b/src/main/java/org/opentripplanner/common/geometry/Serializable2DPackedCoordinateSequenceFactory.java @@ -2,9 +2,9 @@ import java.io.Serializable; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.CoordinateSequenceFactory; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequenceFactory; public class Serializable2DPackedCoordinateSequenceFactory implements Serializable, CoordinateSequenceFactory { @@ -13,7 +13,6 @@ public class Serializable2DPackedCoordinateSequenceFactory implements Serializab @Override public CoordinateSequence create(Coordinate[] coordinates) { return new PackedCoordinateSequence.Double(coordinates, 2); - //return new IntPackedCoordinateSequence(coordinates); } @Override @@ -23,8 +22,7 @@ public CoordinateSequence create(CoordinateSequence coordSeq) { @Override public CoordinateSequence create(int size, int dimension) { - throw new UnsupportedOperationException(); + return new PackedCoordinateSequence.Double(size, dimension); } - } diff --git a/src/main/java/org/opentripplanner/common/geometry/SparseMatrixZSampleGrid.java b/src/main/java/org/opentripplanner/common/geometry/SparseMatrixZSampleGrid.java index 20fd8cc538f..646b103be48 100644 --- a/src/main/java/org/opentripplanner/common/geometry/SparseMatrixZSampleGrid.java +++ b/src/main/java/org/opentripplanner/common/geometry/SparseMatrixZSampleGrid.java @@ -4,7 +4,7 @@ import java.util.Iterator; import java.util.List; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * A generic indexed grid of Z samples. diff --git a/src/main/java/org/opentripplanner/common/geometry/SphericalDistanceLibrary.java b/src/main/java/org/opentripplanner/common/geometry/SphericalDistanceLibrary.java index 3e77a3030dd..50f570f37c1 100644 --- a/src/main/java/org/opentripplanner/common/geometry/SphericalDistanceLibrary.java +++ b/src/main/java/org/opentripplanner/common/geometry/SphericalDistanceLibrary.java @@ -10,10 +10,10 @@ import org.apache.commons.math3.util.FastMath; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.Point; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; public abstract class SphericalDistanceLibrary { diff --git a/src/main/java/org/opentripplanner/common/geometry/Subgraph.java b/src/main/java/org/opentripplanner/common/geometry/Subgraph.java index 6757ea1c1cd..5d9e1c964e5 100644 --- a/src/main/java/org/opentripplanner/common/geometry/Subgraph.java +++ b/src/main/java/org/opentripplanner/common/geometry/Subgraph.java @@ -8,10 +8,10 @@ import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.vertextype.TransitVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.MultiPoint; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.MultiPoint; public class Subgraph { diff --git a/src/main/java/org/opentripplanner/common/geometry/ZSampleGrid.java b/src/main/java/org/opentripplanner/common/geometry/ZSampleGrid.java index 7d3d875879b..20dcdab3834 100644 --- a/src/main/java/org/opentripplanner/common/geometry/ZSampleGrid.java +++ b/src/main/java/org/opentripplanner/common/geometry/ZSampleGrid.java @@ -2,7 +2,7 @@ import org.opentripplanner.common.geometry.ZSampleGrid.ZSamplePoint; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * A generic indexed grid of TZ samples. TZ could be anything but is usually a vector of parameters. diff --git a/src/main/java/org/opentripplanner/common/model/GenericLocation.java b/src/main/java/org/opentripplanner/common/model/GenericLocation.java index abce800e442..fc7fe652b4f 100644 --- a/src/main/java/org/opentripplanner/common/model/GenericLocation.java +++ b/src/main/java/org/opentripplanner/common/model/GenericLocation.java @@ -4,7 +4,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; /** * Class describing a location provided by clients of routing. Used to describe end points (origin, destination) of a routing request as well as any @@ -54,8 +54,10 @@ public class GenericLocation implements Cloneable, Serializable { private static final String _doublePattern = "-{0,1}\\d+(\\.\\d+){0,1}"; // We want to ignore any number of non-digit characters at the beginning of the string, except - // that signs are also non-digits. So ignore any number of non-(digit or sign or decimal point). - private static final Pattern _latLonPattern = Pattern.compile("[^[\\d&&[-|+|.]]]*(" + _doublePattern + // that signs are also non-digits. So ignore any number of non-(digit or sign or decimal point). + // Regex has been rewritten following https://bugs.openjdk.java.net/browse/JDK-8189343 + // from "[^[\\d&&[-|+|.]]]*(" to "[\\D&&[^-+.]]*(" + private static final Pattern _latLonPattern = Pattern.compile("[\\D&&[^-+.]]*(" + _doublePattern + ")(\\s*,\\s*|\\s+)(" + _doublePattern + ")\\D*"); private static final Pattern _headingPattern = Pattern.compile("\\D*heading=(" diff --git a/src/main/java/org/opentripplanner/geocoder/AlternatingGeocoder.java b/src/main/java/org/opentripplanner/geocoder/AlternatingGeocoder.java index b9d36adbe9f..506d6212504 100644 --- a/src/main/java/org/opentripplanner/geocoder/AlternatingGeocoder.java +++ b/src/main/java/org/opentripplanner/geocoder/AlternatingGeocoder.java @@ -1,6 +1,6 @@ package org.opentripplanner.geocoder; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public class AlternatingGeocoder implements Geocoder { diff --git a/src/main/java/org/opentripplanner/geocoder/BackupGeocoder.java b/src/main/java/org/opentripplanner/geocoder/BackupGeocoder.java index 262a5c3a6fe..c865ed2ab3f 100644 --- a/src/main/java/org/opentripplanner/geocoder/BackupGeocoder.java +++ b/src/main/java/org/opentripplanner/geocoder/BackupGeocoder.java @@ -1,6 +1,6 @@ package org.opentripplanner.geocoder; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; /** * Multiplexe two geocoders: a master and a backup. diff --git a/src/main/java/org/opentripplanner/geocoder/Geocoder.java b/src/main/java/org/opentripplanner/geocoder/Geocoder.java index b6a08dedebe..3fb093c8138 100644 --- a/src/main/java/org/opentripplanner/geocoder/Geocoder.java +++ b/src/main/java/org/opentripplanner/geocoder/Geocoder.java @@ -1,6 +1,6 @@ package org.opentripplanner.geocoder; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public interface Geocoder { diff --git a/src/main/java/org/opentripplanner/geocoder/GeocoderGeoZoneCropper.java b/src/main/java/org/opentripplanner/geocoder/GeocoderGeoZoneCropper.java index cac20b40d9e..0bd563c4280 100644 --- a/src/main/java/org/opentripplanner/geocoder/GeocoderGeoZoneCropper.java +++ b/src/main/java/org/opentripplanner/geocoder/GeocoderGeoZoneCropper.java @@ -7,7 +7,7 @@ import org.opentripplanner.geocoder.GeocoderResult; import org.opentripplanner.geocoder.GeocoderResults; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; /** * Filter results of a geocoding request by removing elements outside of the covered geographical diff --git a/src/main/java/org/opentripplanner/geocoder/GeocoderMultipleResultsStubImpl.java b/src/main/java/org/opentripplanner/geocoder/GeocoderMultipleResultsStubImpl.java index bb67cb6bfe0..108a691d80f 100644 --- a/src/main/java/org/opentripplanner/geocoder/GeocoderMultipleResultsStubImpl.java +++ b/src/main/java/org/opentripplanner/geocoder/GeocoderMultipleResultsStubImpl.java @@ -2,7 +2,7 @@ import java.util.Collection; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public class GeocoderMultipleResultsStubImpl implements Geocoder { diff --git a/src/main/java/org/opentripplanner/geocoder/GeocoderNullImpl.java b/src/main/java/org/opentripplanner/geocoder/GeocoderNullImpl.java index a9526cd8d8c..9296901528c 100644 --- a/src/main/java/org/opentripplanner/geocoder/GeocoderNullImpl.java +++ b/src/main/java/org/opentripplanner/geocoder/GeocoderNullImpl.java @@ -1,6 +1,6 @@ package org.opentripplanner.geocoder; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public class GeocoderNullImpl implements Geocoder { diff --git a/src/main/java/org/opentripplanner/geocoder/GeocoderResults.java b/src/main/java/org/opentripplanner/geocoder/GeocoderResults.java index 46e19dd8804..dbaa784f725 100644 --- a/src/main/java/org/opentripplanner/geocoder/GeocoderResults.java +++ b/src/main/java/org/opentripplanner/geocoder/GeocoderResults.java @@ -3,14 +3,9 @@ import java.util.ArrayList; import java.util.Collection; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; - import com.fasterxml.jackson.annotation.JsonProperty; -@XmlRootElement public class GeocoderResults { private String error; @@ -26,7 +21,6 @@ public GeocoderResults(Collection results) { this.results = results; } - @XmlElement(required=false) public String getError() { return error; } @@ -36,9 +30,6 @@ public void setError(String error) { this.error = error; } - - @XmlElementWrapper(name="results") - @XmlElement(name="result") @JsonProperty(value="results") public Collection getResults() { return results; @@ -55,7 +46,6 @@ public void addResult(GeocoderResult result) { results.add(result); } - @XmlElement(name="count") public int getCount() { return results != null ? results.size() : 0; } diff --git a/src/main/java/org/opentripplanner/geocoder/GeocoderStubImpl.java b/src/main/java/org/opentripplanner/geocoder/GeocoderStubImpl.java index 3cb4ea7ca12..3886f5097ec 100644 --- a/src/main/java/org/opentripplanner/geocoder/GeocoderStubImpl.java +++ b/src/main/java/org/opentripplanner/geocoder/GeocoderStubImpl.java @@ -2,7 +2,7 @@ import java.util.Arrays; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public class GeocoderStubImpl implements Geocoder { diff --git a/src/main/java/org/opentripplanner/geocoder/GeocoderUSCSV.java b/src/main/java/org/opentripplanner/geocoder/GeocoderUSCSV.java index 1b682b64750..d37c0433769 100644 --- a/src/main/java/org/opentripplanner/geocoder/GeocoderUSCSV.java +++ b/src/main/java/org/opentripplanner/geocoder/GeocoderUSCSV.java @@ -12,7 +12,7 @@ import javax.ws.rs.core.UriBuilder; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public class GeocoderUSCSV implements Geocoder { diff --git a/src/main/java/org/opentripplanner/geocoder/bano/BanoGeocoder.java b/src/main/java/org/opentripplanner/geocoder/bano/BanoGeocoder.java index 61b66f8a29e..95a0ec3af10 100644 --- a/src/main/java/org/opentripplanner/geocoder/bano/BanoGeocoder.java +++ b/src/main/java/org/opentripplanner/geocoder/bano/BanoGeocoder.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.geojson.Feature; import org.geojson.FeatureCollection; import org.geojson.GeoJsonObject; diff --git a/src/main/java/org/opentripplanner/geocoder/google/GoogleGeocoder.java b/src/main/java/org/opentripplanner/geocoder/google/GoogleGeocoder.java index ce9a5eeffb4..4b7d35a938e 100644 --- a/src/main/java/org/opentripplanner/geocoder/google/GoogleGeocoder.java +++ b/src/main/java/org/opentripplanner/geocoder/google/GoogleGeocoder.java @@ -1,6 +1,6 @@ package org.opentripplanner.geocoder.google; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.geocoder.Geocoder; import org.opentripplanner.geocoder.GeocoderResult; import org.opentripplanner.geocoder.GeocoderResults; diff --git a/src/main/java/org/opentripplanner/geocoder/nominatim/NominatimGeocoder.java b/src/main/java/org/opentripplanner/geocoder/nominatim/NominatimGeocoder.java index 31285e4dcee..c11d34ecedc 100644 --- a/src/main/java/org/opentripplanner/geocoder/nominatim/NominatimGeocoder.java +++ b/src/main/java/org/opentripplanner/geocoder/nominatim/NominatimGeocoder.java @@ -1,6 +1,6 @@ package org.opentripplanner.geocoder.nominatim; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.geocoder.Geocoder; import org.opentripplanner.geocoder.GeocoderResult; import org.opentripplanner.geocoder.GeocoderResults; diff --git a/src/main/java/org/opentripplanner/geocoder/reverse/MunicoderServer.java b/src/main/java/org/opentripplanner/geocoder/reverse/MunicoderServer.java index 72aa28d7a73..01332c7a1b3 100644 --- a/src/main/java/org/opentripplanner/geocoder/reverse/MunicoderServer.java +++ b/src/main/java/org/opentripplanner/geocoder/reverse/MunicoderServer.java @@ -8,8 +8,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -/*import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope;*/ +/*import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope;*/ @Path("/municoder") diff --git a/src/main/java/org/opentripplanner/geocoder/reverse/ShapefileBoundaryResolver.java b/src/main/java/org/opentripplanner/geocoder/reverse/ShapefileBoundaryResolver.java index c14736ef869..d7840a29fab 100644 --- a/src/main/java/org/opentripplanner/geocoder/reverse/ShapefileBoundaryResolver.java +++ b/src/main/java/org/opentripplanner/geocoder/reverse/ShapefileBoundaryResolver.java @@ -11,10 +11,10 @@ import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeature; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.Point; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; public class ShapefileBoundaryResolver implements BoundaryResolver { diff --git a/src/main/java/org/opentripplanner/geocoder/yahoo/YahooGeocoder.java b/src/main/java/org/opentripplanner/geocoder/yahoo/YahooGeocoder.java index bdf9e396765..89f44b689e4 100644 --- a/src/main/java/org/opentripplanner/geocoder/yahoo/YahooGeocoder.java +++ b/src/main/java/org/opentripplanner/geocoder/yahoo/YahooGeocoder.java @@ -1,6 +1,6 @@ package org.opentripplanner.geocoder.yahoo; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.geocoder.Geocoder; import org.opentripplanner.geocoder.GeocoderResult; import org.opentripplanner.geocoder.GeocoderResults; diff --git a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java index 6f1dcd5bc56..d8f338b5f3b 100644 --- a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java @@ -24,7 +24,6 @@ import org.opentripplanner.reflect.ReflectionLibrary; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.graph.Graph.LoadLevel; import org.opentripplanner.standalone.CommandLineParameters; import org.opentripplanner.standalone.GraphBuilderParameters; import org.opentripplanner.standalone.OTPMain; @@ -47,7 +46,7 @@ * It is modular: GraphBuilderModules are placed in a list and run in sequence. */ public class GraphBuilder implements Runnable { - + private static Logger LOG = LoggerFactory.getLogger(GraphBuilder.class); public static final String BUILDER_CONFIG_FILENAME = "build-config.json"; @@ -55,13 +54,13 @@ public class GraphBuilder implements Runnable { private List _graphBuilderModules = new ArrayList(); private final File graphFile; - + private boolean _alwaysRebuild = true; private List modeList; - + private String baseGraph = null; - + private Graph graph = new Graph(); /** Should the graph be serialized to disk after being created or not? */ @@ -83,11 +82,11 @@ public void setGraphBuilders(List graphLoaders) { public void setAlwaysRebuild(boolean alwaysRebuild) { _alwaysRebuild = alwaysRebuild; } - + public void setBaseGraph(String baseGraph) { this.baseGraph = baseGraph; try { - graph = Graph.load(new File(baseGraph), LoadLevel.FULL); + graph = Graph.load(new File(baseGraph)); } catch (Exception e) { throw new RuntimeException("error loading base graph"); } @@ -100,7 +99,7 @@ public void addMode(RoutingRequest mo) { public void setModes(List modeList) { this.modeList = modeList; } - + public Graph getGraph() { return this.graph; } @@ -110,7 +109,7 @@ public void run() { long startTime = System.currentTimeMillis(); if (serializeGraph) { - + if (graphFile == null) { throw new RuntimeException("graphBuilderTask has no attribute graphFile."); } @@ -119,7 +118,7 @@ public void run() { LOG.info("graph already exists and alwaysRebuild=false => skipping graph build"); return; } - + try { if (!graphFile.getParentFile().exists()) { if (!graphFile.getParentFile().mkdirs()) { @@ -193,24 +192,24 @@ public static GraphBuilder forDirectory(CommandLineParameters params, File dir) for (File file : dir.listFiles()) { switch (InputFileType.forFile(file)) { - case GTFS: - LOG.info("Found GTFS file {}", file); - gtfsFiles.add(file); - break; - case OSM: - LOG.info("Found OSM file {}", file); - osmFiles.add(file); - break; - case DEM: - if (!builderParams.fetchElevationUS && demFile == null) { - LOG.info("Found DEM file {}", file); - demFile = file; - } else { - LOG.info("Skipping DEM file {}", file); - } - break; - case OTHER: - LOG.warn("Skipping unrecognized file '{}'", file); + case GTFS: + LOG.info("Found GTFS file {}", file); + gtfsFiles.add(file); + break; + case OSM: + LOG.info("Found OSM file {}", file); + osmFiles.add(file); + break; + case DEM: + if (!builderParams.fetchElevationUS && demFile == null) { + LOG.info("Found DEM file {}", file); + demFile = file; + } else { + LOG.info("Skipping DEM file {}", file); + } + break; + case OTHER: + LOG.warn("Skipping unrecognized file '{}'", file); } } boolean hasOSM = builderParams.streets && !osmFiles.isEmpty(); @@ -299,7 +298,8 @@ public static GraphBuilder forDirectory(CommandLineParameters params, File dir) params.cacheDirectory, builderParams.readCachedElevations, builderParams.writeCachedElevations, - builderParams.includeEllipsoidToGeoidDifference + builderParams.includeEllipsoidToGeoidDifference, + builderParams.elevationUnitMultiplier ) ); } diff --git a/src/main/java/org/opentripplanner/graph_builder/GraphStats.java b/src/main/java/org/opentripplanner/graph_builder/GraphStats.java index 59e70e0dcb5..148ff5bb47e 100644 --- a/src/main/java/org/opentripplanner/graph_builder/GraphStats.java +++ b/src/main/java/org/opentripplanner/graph_builder/GraphStats.java @@ -29,10 +29,10 @@ import com.csvreader.CsvWriter; import com.google.common.collect.Multiset; import com.google.common.collect.TreeMultiset; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.linearref.LinearLocation; -import com.vividsolutions.jts.linearref.LocationIndexedLine; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.linearref.LinearLocation; +import org.locationtech.jts.linearref.LocationIndexedLine; public class GraphStats { @@ -95,7 +95,7 @@ private void run() { /* open input graph (same for all commands) */ File graphFile = new File(graphPath); try { - graph = Graph.load(graphFile, Graph.LoadLevel.FULL); + graph = Graph.load(graphFile); } catch (Exception e) { LOG.error("Exception while loading graph from " + graphFile); return; diff --git a/src/main/java/org/opentripplanner/graph_builder/linking/SampleStopLinker.java b/src/main/java/org/opentripplanner/graph_builder/linking/SampleStopLinker.java index 20db1478352..d9fef86d8c5 100644 --- a/src/main/java/org/opentripplanner/graph_builder/linking/SampleStopLinker.java +++ b/src/main/java/org/opentripplanner/graph_builder/linking/SampleStopLinker.java @@ -3,9 +3,9 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.analyst.core.Sample; import org.opentripplanner.analyst.request.SampleFactory; import org.opentripplanner.common.geometry.GeometryUtils; diff --git a/src/main/java/org/opentripplanner/graph_builder/linking/StreetSplitter.java b/src/main/java/org/opentripplanner/graph_builder/linking/StreetSplitter.java index e5d10b21db7..ba6eac68692 100644 --- a/src/main/java/org/opentripplanner/graph_builder/linking/StreetSplitter.java +++ b/src/main/java/org/opentripplanner/graph_builder/linking/StreetSplitter.java @@ -1,13 +1,13 @@ package org.opentripplanner.graph_builder.linking; import com.google.common.collect.Iterables; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.index.SpatialIndex; -import com.vividsolutions.jts.linearref.LinearLocation; -import com.vividsolutions.jts.linearref.LocationIndexedLine; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.index.SpatialIndex; +import org.locationtech.jts.linearref.LinearLocation; +import org.locationtech.jts.linearref.LocationIndexedLine; import gnu.trove.map.TIntDoubleMap; import gnu.trove.map.hash.TIntDoubleHashMap; import jersey.repackaged.com.google.common.collect.Lists; @@ -179,7 +179,9 @@ public StreetSplitter(Graph graph) { */ public void linkAllStationsToGraph() { for (Vertex v : graph.getVertices()) { - if (v instanceof TransitStop || v instanceof BikeRentalStationVertex || v instanceof BikeParkVertex) + if (v instanceof TransitStop || v instanceof BikeRentalStationVertex || v instanceof BikeParkVertex) { + boolean alreadyLinked = v.getOutgoing().stream().anyMatch(e -> e instanceof StreetTransitLink); + if (alreadyLinked) continue; if (!linkToClosestWalkableEdge(v, DESTRUCTIVE_SPLIT, false)) { if (v instanceof TransitStop) LOG.warn(graph.addBuilderAnnotation(new StopUnlinked((TransitStop) v))); @@ -188,6 +190,7 @@ else if (v instanceof BikeRentalStationVertex) else if (v instanceof BikeParkVertex) LOG.warn(graph.addBuilderAnnotation(new BikeParkUnlinked((BikeParkVertex) v))); }; + } } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/CheckGeometryModule.java b/src/main/java/org/opentripplanner/graph_builder/module/CheckGeometryModule.java index adccb78eb72..51a6ff2ee51 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/CheckGeometryModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/CheckGeometryModule.java @@ -16,8 +16,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; /** * Check the geometry of every edge in the graph for any bogus geometry -- diff --git a/src/main/java/org/opentripplanner/graph_builder/module/GraphStatisticsModule.java b/src/main/java/org/opentripplanner/graph_builder/module/GraphStatisticsModule.java index 6d5a6539f43..798e489ecbd 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/GraphStatisticsModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/GraphStatisticsModule.java @@ -16,7 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; /** * Print statistics on geometry and edge/vertices data for a graph (number of geometry, average diff --git a/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java b/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java index 492453e1c0c..4bcc6517f3c 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java @@ -2,9 +2,9 @@ import com.beust.jcommander.internal.Lists; import com.beust.jcommander.internal.Sets; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.api.resource.CoordinateArrayListSequence; import org.opentripplanner.api.resource.SimpleIsochrone; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/TransitStopsRegionsSourceImpl.java b/src/main/java/org/opentripplanner/graph_builder/module/TransitStopsRegionsSourceImpl.java index a3ecd7e0ee6..a971905a8a5 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/TransitStopsRegionsSourceImpl.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/TransitStopsRegionsSourceImpl.java @@ -10,8 +10,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; /** * A rectangular region bounding a set of transit stops diff --git a/src/main/java/org/opentripplanner/graph_builder/module/TransitToTaggedStopsModule.java b/src/main/java/org/opentripplanner/graph_builder/module/TransitToTaggedStopsModule.java index 3f12cf3ea25..2c4cc7bb1a6 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/TransitToTaggedStopsModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/TransitToTaggedStopsModule.java @@ -1,7 +1,7 @@ package org.opentripplanner.graph_builder.module; import com.google.common.collect.Iterables; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.graph_builder.services.GraphBuilderModule; import org.opentripplanner.routing.edgetype.StreetTransitLink; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/map/EndMatchState.java b/src/main/java/org/opentripplanner/graph_builder/module/map/EndMatchState.java new file mode 100644 index 00000000000..aba731a4d79 --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/map/EndMatchState.java @@ -0,0 +1,22 @@ +package org.opentripplanner.graph_builder.module.map; + +import java.util.List; + +/** + * The end of a route's geometry, meaning that the search can quit + * @author novalis + * + */ +public class EndMatchState extends MatchState { + + public EndMatchState(MatchState parent, double error, double distance) { + super(parent, null, distance); + this.currentError = error; + } + + @Override + public List getNextStates() { + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/graph_builder/module/map/LinearIterator.java b/src/main/java/org/opentripplanner/graph_builder/module/map/LinearIterator.java new file mode 100644 index 00000000000..1abec894484 --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/map/LinearIterator.java @@ -0,0 +1,220 @@ +package org.opentripplanner.graph_builder.module.map; + +import java.util.Iterator; + +import org.locationtech.jts.geom.*; +import org.locationtech.jts.linearref.LinearLocation; + +/** + * I copied this class from JTS but made a few changes. + * + * The JTS version of this class has several design decisions that don't work for me. In particular, + * hasNext() in the original should be "isValid", and if we start mid-segment, we should continue at + * the end of this segment rather than the end of the next segment. + */ +public class LinearIterator implements Iterable { + + private Geometry linearGeom; + + private final int numLines; + + /** + * Invariant: currentLine <> null if the iterator is pointing at a valid coordinate + * + * @throws IllegalArgumentException if linearGeom is not lineal + */ + private LineString currentLine; + + private int componentIndex = 0; + + private int vertexIndex = 0; + + private double segmentFraction; + + /** + * Creates an iterator initialized to the start of a linear {@link Geometry} + * + * @param linear the linear geometry to iterate over + * @throws IllegalArgumentException if linearGeom is not lineal + */ + public LinearIterator(Geometry linear) { + this(linear, 0, 0); + } + + /** + * Creates an iterator starting at a {@link LinearLocation} on a linear {@link Geometry} + * + * @param linear the linear geometry to iterate over + * @param start the location to start at + * @throws IllegalArgumentException if linearGeom is not lineal + */ + public LinearIterator(Geometry linear, LinearLocation start) { + this(linear, start.getComponentIndex(), start.getSegmentIndex()); + this.segmentFraction = start.getSegmentFraction(); + } + + /** + * Creates an iterator starting at a specified component and vertex in a linear {@link Geometry} + * + * @param linearGeom the linear geometry to iterate over + * @param componentIndex the component to start at + * @param vertexIndex the vertex to start at + * @throws IllegalArgumentException if linearGeom is not lineal + */ + public LinearIterator(Geometry linearGeom, int componentIndex, int vertexIndex) { + if (!(linearGeom instanceof Lineal)) + throw new IllegalArgumentException("Lineal geometry is required"); + this.linearGeom = linearGeom; + numLines = linearGeom.getNumGeometries(); + this.componentIndex = componentIndex; + this.vertexIndex = vertexIndex; + loadCurrentLine(); + } + + private void loadCurrentLine() { + if (componentIndex >= numLines) { + currentLine = null; + return; + } + currentLine = (LineString) linearGeom.getGeometryN(componentIndex); + } + + /** + * Tests whether there are any vertices left to iterator over. + * + * @return true if there are more vertices to scan + */ + public boolean hasNext() { + if (componentIndex >= numLines) + return false; + if (componentIndex == numLines - 1 && vertexIndex >= currentLine.getNumPoints() - 1) + return false; + return true; + } + + public boolean isValidIndex() { + if (componentIndex >= numLines) + return false; + if (componentIndex == numLines - 1 && vertexIndex >= currentLine.getNumPoints()) + return false; + return true; + } + + /** + * Moves the iterator ahead to the next vertex and (possibly) linear component. + */ + public void next() { + if (!hasNext()) + return; + segmentFraction = 0.0; + vertexIndex++; + if (vertexIndex >= currentLine.getNumPoints()) { + componentIndex++; + if (componentIndex < linearGeom.getNumGeometries() - 1) { + loadCurrentLine(); + vertexIndex = 0; + } + } + } + + /** + * Checks whether the iterator cursor is pointing to the endpoint of a linestring. + * + * @return true if the iterator is at an endpoint + */ + public boolean isEndOfLine() { + if (componentIndex >= numLines) + return false; + // LineString currentLine = (LineString) linear.getGeometryN(componentIndex); + if (vertexIndex < currentLine.getNumPoints() - 1) + return false; + return true; + } + + /** + * The component index of the vertex the iterator is currently at. + * + * @return the current component index + */ + public int getComponentIndex() { + return componentIndex; + } + + /** + * The vertex index of the vertex the iterator is currently at. + * + * @return the current vertex index + */ + public int getVertexIndex() { + return vertexIndex; + } + + /** + * Gets the {@link LineString} component the iterator is current at. + * + * @return a linestring + */ + public LineString getLine() { + return currentLine; + } + + /** + * Gets the first {@link Coordinate} of the current segment. (the coordinate of the current + * vertex). + * + * @return a {@link Coordinate} + */ + public Coordinate getSegmentStart() { + return currentLine.getCoordinateN(vertexIndex); + } + + /** + * Gets the second {@link Coordinate} of the current segment. (the coordinate of the next + * vertex). If the iterator is at the end of a line, null is returned. + * + * @return a {@link Coordinate} or null + */ + public Coordinate getSegmentEnd() { + if (vertexIndex < getLine().getNumPoints() - 1) + return currentLine.getCoordinateN(vertexIndex + 1); + return null; + } + + public LinearLocation getLocation() { + return new LinearLocation(componentIndex, vertexIndex, segmentFraction); + } + + class LinearIteratorIterator implements Iterator { + + @Override + public boolean hasNext() { + return LinearIterator.this.hasNext(); + } + + @Override + public LinearLocation next() { + LinearLocation result = getLocation(); + LinearIterator.this.next(); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + } + @Override + public Iterator iterator() { + return new LinearIteratorIterator(); + } + + public static LinearLocation getEndLocation(Geometry linear) { + //the version in LinearLocation is broken + + int lastComponentIndex = linear.getNumGeometries() - 1; + LineString lastLine = (LineString) linear.getGeometryN(lastComponentIndex); + int lastSegmentIndex = lastLine.getNumPoints() - 1; + return new LinearLocation(lastComponentIndex, lastSegmentIndex, 0.0); + } +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/graph_builder/module/map/MatchState.java b/src/main/java/org/opentripplanner/graph_builder/module/map/MatchState.java new file mode 100644 index 00000000000..b123919ca8a --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/map/MatchState.java @@ -0,0 +1,115 @@ +package org.opentripplanner.graph_builder.module.map; + +import java.util.ArrayList; +import java.util.List; + +import org.opentripplanner.common.geometry.SphericalDistanceLibrary; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.core.TraverseMode; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Vertex; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.linearref.LinearLocation; + +public abstract class MatchState { + private static final RoutingRequest traverseOptions = new RoutingRequest(TraverseMode.CAR); + + protected static final double NEW_SEGMENT_PENALTY = 0.1; + + protected static final double NO_TRAVERSE_PENALTY = 20; + + public double currentError; + + public double accumulatedError; + + public MatchState parent; + + protected Edge edge; + + private double distanceAlongRoute = 0; + + public MatchState(MatchState parent, Edge edge, double distanceAlongRoute) { + this.distanceAlongRoute = distanceAlongRoute; + this.parent = parent; + this.edge = edge; + if (parent != null) { + this.accumulatedError = parent.accumulatedError + parent.currentError; + this.distanceAlongRoute += parent.distanceAlongRoute; + } + } + + public abstract List getNextStates(); + + public Edge getEdge() { + return edge; + } + + public double getTotalError() { + return accumulatedError + currentError; + } + + protected boolean carsCanTraverse(Edge edge) { + // should be done with a method on edge (canTraverse already exists on turnEdge) + State s0 = new State(edge.getFromVertex(), traverseOptions); + State s1 = edge.traverse(s0); + return s1 != null; + } + + protected List getOutgoingMatchableEdges(Vertex vertex) { + List edges = new ArrayList(); + for (Edge e : vertex.getOutgoing()) { + if (!(e instanceof StreetEdge)) + continue; + if (e.getGeometry() == null) + continue; + edges.add(e); + } + return edges; + } + + + public double getDistanceAlongRoute() { + return distanceAlongRoute; + } + + /* computes the distance, in meters, along a geometry */ + protected static double distanceAlongGeometry(Geometry geometry, LinearLocation startIndex, + LinearLocation endIndex) { + + if (endIndex == null) { + endIndex = LinearLocation.getEndLocation(geometry); + } + double total = 0; + LinearIterator it = new LinearIterator(geometry, startIndex); + LinearLocation index = startIndex; + Coordinate previousCoordinate = startIndex.getCoordinate(geometry); + + it.next(); + index = it.getLocation(); + while (index.compareTo(endIndex) < 0) { + Coordinate thisCoordinate = index.getCoordinate(geometry); + double distance = SphericalDistanceLibrary.fastDistance(previousCoordinate, thisCoordinate); + total += distance; + previousCoordinate = thisCoordinate; + if (!it.hasNext()) + break; + it.next(); + index = it.getLocation(); + } + //now, last bit of last segment + Coordinate finalCoordinate = endIndex.getCoordinate(geometry); + total += SphericalDistanceLibrary.distance(previousCoordinate, finalCoordinate); + + return total; + } + + + protected static double distance(Coordinate from, Coordinate to) { + return SphericalDistanceLibrary.fastDistance(from, to); + } + +} diff --git a/src/main/java/org/opentripplanner/graph_builder/module/map/MidblockMatchState.java b/src/main/java/org/opentripplanner/graph_builder/module/map/MidblockMatchState.java new file mode 100644 index 00000000000..e63816100e0 --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/map/MidblockMatchState.java @@ -0,0 +1,234 @@ +package org.opentripplanner.graph_builder.module.map; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Vertex; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.linearref.LinearLocation; +import org.locationtech.jts.linearref.LocationIndexedLine; +import org.locationtech.jts.util.AssertionFailedException; + +public class MidblockMatchState extends MatchState { + + private static final double MAX_ERROR = 1000; + + private LinearLocation edgeIndex; + + public LinearLocation routeIndex; + + Geometry routeGeometry; + + private Geometry edgeGeometry; + + private LocationIndexedLine indexedEdge; + + public MidblockMatchState(MatchState parent, Geometry routeGeometry, Edge edge, + LinearLocation routeIndex, LinearLocation edgeIndex, double error, + double distanceAlongRoute) { + super(parent, edge, distanceAlongRoute); + + this.routeGeometry = routeGeometry; + this.routeIndex = routeIndex; + this.edgeIndex = edgeIndex; + + edgeGeometry = edge.getGeometry(); + indexedEdge = new LocationIndexedLine(edgeGeometry); + currentError = error; + } + + @Override + public List getNextStates() { + ArrayList nextStates = new ArrayList(); + if (routeIndex.getSegmentIndex() == routeGeometry.getNumPoints() - 1) { + // this has either hit the end, or gone off the end. It's not real clear which. + // for now, let's assume it means that the ending is somewhere along this edge, + // so we return an end state + Coordinate pt = routeIndex.getCoordinate(routeGeometry); + double error = distance(pt, edgeIndex.getCoordinate(edgeGeometry)); + nextStates.add(new EndMatchState(this, error, 0)); + return nextStates; + } + + LinearIterator it = new LinearIterator(routeGeometry, routeIndex); + if (it.hasNext()) { + it.next(); + LinearLocation routeSuccessor = it.getLocation(); + + // now we want to see where this new point is in terms of the edge's geometry + Coordinate newRouteCoord = routeSuccessor.getCoordinate(routeGeometry); + LinearLocation newEdgeIndex = indexedEdge.project(newRouteCoord); + + Coordinate edgeCoord = newEdgeIndex.getCoordinate(edgeGeometry); + if (newEdgeIndex.compareTo(edgeIndex) <= 0) { + // we must make forward progress along the edge... or go to the next edge + /* this should not require the try/catch, but there is a bug in JTS */ + try { + LinearLocation projected2 = indexedEdge.indexOfAfter(edgeCoord, edgeIndex); + //another bug in JTS + if (Double.isNaN(projected2.getSegmentFraction())) { + // we are probably moving backwards + return Collections.emptyList(); + } else { + newEdgeIndex = projected2; + if (newEdgeIndex.equals(edgeIndex)) { + return Collections.emptyList(); + } + } + edgeCoord = newEdgeIndex.getCoordinate(edgeGeometry); + } catch (AssertionFailedException e) { + // we are not making progress, so just return an empty list + return Collections.emptyList(); + } + } + + if (newEdgeIndex.getSegmentIndex() == edgeGeometry.getNumPoints() - 1) { + + // we might choose to continue from the end of the edge and a point mid-way + // along this route segment + + // find nearest point that makes progress along the route + Vertex toVertex = edge.getToVertex(); + Coordinate endCoord = toVertex.getCoordinate(); + LocationIndexedLine indexedRoute = new LocationIndexedLine(routeGeometry); + + // FIXME: it would be better to do this project/indexOfAfter in one step + // as the two-step version could snap to a bad place and be unable to escape. + + LinearLocation routeProjectedEndIndex = indexedRoute.project(endCoord); + Coordinate routeProjectedEndCoord = routeProjectedEndIndex + .getCoordinate(routeGeometry); + + if (routeProjectedEndIndex.compareTo(routeIndex) <= 0) { + try { + routeProjectedEndIndex = indexedRoute.indexOfAfter(routeProjectedEndCoord, + routeIndex); + if (Double.isNaN(routeProjectedEndIndex.getSegmentFraction())) { + // can't go forward + routeProjectedEndIndex = routeIndex; // this is bad, but not terrible + // since we are advancing along the edge + } + } catch (AssertionFailedException e) { + routeProjectedEndIndex = routeIndex; + } + routeProjectedEndCoord = routeProjectedEndIndex.getCoordinate(routeGeometry); + } + + double positionError = distance(routeProjectedEndCoord, endCoord); + double travelAlongRoute = distanceAlongGeometry(routeGeometry, routeIndex, + routeProjectedEndIndex); + double travelAlongEdge = distanceAlongGeometry(edgeGeometry, edgeIndex, + newEdgeIndex); + double travelError = Math.abs(travelAlongEdge - travelAlongRoute); + + double error = positionError + travelError; + + if (error > MAX_ERROR) { + // we're not going to bother with states which are + // totally wrong + return nextStates; + } + + for (Edge e : getOutgoingMatchableEdges(toVertex)) { + double cost = error + NEW_SEGMENT_PENALTY; + if (!carsCanTraverse(e)) { + cost += NO_TRAVERSE_PENALTY; + } + MatchState nextState = new MidblockMatchState(this, routeGeometry, e, + routeProjectedEndIndex, new LinearLocation(), cost, travelAlongRoute); + nextStates.add(nextState); + } + + } else { + + double travelAlongEdge = distanceAlongGeometry(edgeGeometry, edgeIndex, + newEdgeIndex); + double travelAlongRoute = distanceAlongGeometry(routeGeometry, routeIndex, + routeSuccessor); + double travelError = Math.abs(travelAlongRoute - travelAlongEdge); + + double positionError = distance(edgeCoord, newRouteCoord); + + double error = travelError + positionError; + + MatchState nextState = new MidblockMatchState(this, routeGeometry, edge, + routeSuccessor, newEdgeIndex, error, travelAlongRoute); + nextStates.add(nextState); + + // it's also possible that, although we have not yet reached the end of this edge, + // we are going to turn, because the route turns earlier than the edge. In that + // case, we jump to the corner, and our error is the distance from the route point + // and the corner + + Vertex toVertex = edge.getToVertex(); + double travelAlongOldEdge = distanceAlongGeometry(edgeGeometry, edgeIndex, null); + + for (Edge e : getOutgoingMatchableEdges(toVertex)) { + Geometry newEdgeGeometry = e.getGeometry(); + LocationIndexedLine newIndexedEdge = new LocationIndexedLine(newEdgeGeometry); + newEdgeIndex = newIndexedEdge.project(newRouteCoord); + Coordinate newEdgeCoord = newEdgeIndex.getCoordinate(newEdgeGeometry); + positionError = distance(newEdgeCoord, newRouteCoord); + travelAlongEdge = travelAlongOldEdge + distanceAlongGeometry(newEdgeGeometry, new LinearLocation(), newEdgeIndex); + travelError = Math.abs(travelAlongRoute - travelAlongEdge); + + error = travelError + positionError; + + if (error > MAX_ERROR) { + // we're not going to bother with states which are + // totally wrong + return nextStates; + } + + double cost = error + NEW_SEGMENT_PENALTY; + if (!carsCanTraverse(e)) { + cost += NO_TRAVERSE_PENALTY; + } + + nextState = new MidblockMatchState(this, routeGeometry, e, routeSuccessor, + new LinearLocation(), cost, travelAlongRoute); + nextStates.add(nextState); + } + + } + return nextStates; + + } else { + Coordinate routeCoord = routeIndex.getCoordinate(routeGeometry); + LinearLocation projected = indexedEdge.project(routeCoord); + double locationError = distance(projected.getCoordinate(edgeGeometry), routeCoord); + + MatchState end = new EndMatchState(this, locationError, 0); + return Arrays.asList(end); + } + + } + + public String toString() { + return "MidblockMatchState(" + edge + ", " + edgeIndex.getSegmentIndex() + ", " + + edgeIndex.getSegmentFraction() + ") - " + currentError; + } + + public int hashCode() { + return (edge.hashCode() * 1337 + hashCode(edgeIndex)) * 1337 + hashCode(routeIndex); + } + + private int hashCode(LinearLocation location) { + return location.getComponentIndex() * 1000000 + location.getSegmentIndex() * 37 + + new Double(location.getSegmentFraction()).hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof MidblockMatchState)) { + return false; + } + MidblockMatchState other = (MidblockMatchState) o; + return other.edge == edge && other.edgeIndex.compareTo(edgeIndex) == 0 + && other.routeIndex.compareTo(routeIndex) == 0; + } +} diff --git a/src/main/java/org/opentripplanner/graph_builder/module/map/StreetMatcher.java b/src/main/java/org/opentripplanner/graph_builder/module/map/StreetMatcher.java new file mode 100644 index 00000000000..07fcf2a5bc9 --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/map/StreetMatcher.java @@ -0,0 +1,160 @@ +package org.opentripplanner.graph_builder.module.map; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.Vertex; +import org.opentripplanner.common.pqueue.BinHeap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.linearref.LinearLocation; +import org.locationtech.jts.linearref.LocationIndexedLine; +import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; + +/** + * This Performs most of the work for the MapBuilder graph builder module. + * It determines which sequence of graph edges a GTFS shape probably corresponds to. + * Note that GTFS shapes are not in any way constrained to OSM edges or even roads. + */ +public class StreetMatcher { + private static final Logger log = LoggerFactory.getLogger(StreetMatcher.class); + private static final double DISTANCE_THRESHOLD = 0.0002; + + Graph graph; + + private STRtree index; + + STRtree createIndex() { + STRtree edgeIndex = new STRtree(); + for (Vertex v : graph.getVertices()) { + for (Edge e : v.getOutgoing()) { + if (e instanceof StreetEdge) { + Envelope envelope; + Geometry geometry = e.getGeometry(); + envelope = geometry.getEnvelopeInternal(); + edgeIndex.insert(envelope, e); + } + } + } + log.debug("Created index"); + return edgeIndex; + } + + public StreetMatcher(Graph graph) { + this.graph = graph; + index = createIndex(); + index.build(); + } + + @SuppressWarnings("unchecked") + public List match(Geometry routeGeometry) { + + routeGeometry = removeDuplicatePoints(routeGeometry); + + if (routeGeometry == null) + return null; + + routeGeometry = DouglasPeuckerSimplifier.simplify(routeGeometry, 0.00001); + + // initial state: start midway along a block. + LocationIndexedLine indexedLine = new LocationIndexedLine(routeGeometry); + + LinearLocation startIndex = indexedLine.getStartIndex(); + + Coordinate routeStartCoordinate = startIndex.getCoordinate(routeGeometry); + Envelope envelope = new Envelope(routeStartCoordinate); + double distanceThreshold = DISTANCE_THRESHOLD; + envelope.expandBy(distanceThreshold); + + BinHeap states = new BinHeap(); + List nearbyEdges = index.query(envelope); + while (nearbyEdges.isEmpty()) { + envelope.expandBy(distanceThreshold); + distanceThreshold *= 2; + nearbyEdges = index.query(envelope); + } + + // compute initial states + for (Edge initialEdge : nearbyEdges) { + Geometry edgeGeometry = initialEdge.getGeometry(); + + LocationIndexedLine indexedEdge = new LocationIndexedLine(edgeGeometry); + LinearLocation initialLocation = indexedEdge.project(routeStartCoordinate); + + double error = MatchState.distance(initialLocation.getCoordinate(edgeGeometry), routeStartCoordinate); + MidblockMatchState state = new MidblockMatchState(null, routeGeometry, initialEdge, startIndex, initialLocation, error, 0.01); + states.insert(state, 0); //make sure all initial states are visited by inserting them at 0 + } + + // search for best-matching path + int seen_count = 0, total = 0; + HashSet seen = new HashSet(); + while (!states.empty()) { + double k = states.peek_min_key(); + MatchState state = states.extract_min(); + if (++total % 50000 == 0) { + log.debug("seen / total: " + seen_count + " / " + total); + } + if (seen.contains(state)) { + ++seen_count; + continue; + } else { + if (k != 0) { + //but do not mark states as closed if we start at them + seen.add(state); + } + } + if (state instanceof EndMatchState) { + return toEdgeList(state); + } + for (MatchState next : state.getNextStates()) { + if (seen.contains(next)) { + continue; + } + states.insert(next, next.getTotalError() - next.getDistanceAlongRoute()); + } + } + return null; + } + + private Geometry removeDuplicatePoints(Geometry routeGeometry) { + List coords = new ArrayList(); + Coordinate last = null; + for (Coordinate c : routeGeometry.getCoordinates()) { + if (!c.equals(last)) { + last = c; + coords.add(c); + } + } + if (coords.size() < 2) { + return null; + } + Coordinate[] coordArray = new Coordinate[coords.size()]; + return routeGeometry.getFactory().createLineString(coords.toArray(coordArray)); + } + + private List toEdgeList(MatchState next) { + ArrayList edges = new ArrayList(); + Edge lastEdge = null; + while (next != null) { + Edge edge = next.getEdge(); + if (edge != lastEdge) { + edges.add(edge); + lastEdge = edge; + } + next = next.parent; + } + Collections.reverse(edges); + return edges; + } +} diff --git a/src/main/java/org/opentripplanner/graph_builder/module/ned/DegreeGridNEDTileSource.java b/src/main/java/org/opentripplanner/graph_builder/module/ned/DegreeGridNEDTileSource.java index 678d11c98a3..e9104897657 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/ned/DegreeGridNEDTileSource.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/ned/DegreeGridNEDTileSource.java @@ -1,6 +1,6 @@ package org.opentripplanner.graph_builder.module.ned; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import org.jets3t.service.S3Service; import org.jets3t.service.S3ServiceException; import org.jets3t.service.ServiceException; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java b/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java index bccbdaf97b2..2a30dfe51b3 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java @@ -1,7 +1,7 @@ package org.opentripplanner.graph_builder.module.ned; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; import org.geotools.geometry.DirectPosition2D; import org.opengis.coverage.Coverage; import org.opengis.coverage.PointOutsideCoverageException; @@ -67,6 +67,12 @@ public class ElevationModule implements GraphBuilderModule { private final File cachedElevationsFile; /* Whether or not to include geoid difference values in individual elevation calculations */ private final boolean includeEllipsoidToGeoidDifference; + /** + * Unit conversion multiplier for elevation values. No conversion needed if the elevation values + * are defined in meters in the source data. If, for example, decimetres are used in the source data, + * this should be set to 0.1 in build-config.json. + */ + private double elevationUnitMultiplier = 1; private HashMap cachedElevations; @@ -104,13 +110,15 @@ public ElevationModule( File cacheDirectory, boolean readCachedElevations, boolean writeCachedElevations, - boolean includeEllipsoidToGeoidDifference + boolean includeEllipsoidToGeoidDifference, + double elevationUnitMultiplier ) { gridCoverageFactory = factory; cachedElevationsFile = new File(cacheDirectory, "cached_elevations.obj"); this.readCachedElevations = readCachedElevations; this.writeCachedElevations = writeCachedElevations; this.includeEllipsoidToGeoidDifference = includeEllipsoidToGeoidDifference; + this.elevationUnitMultiplier = elevationUnitMultiplier; } public List provides() { @@ -290,7 +298,7 @@ class ElevationRepairState { public double initialElevation; public ElevationRepairState(StreetEdge backEdge, ElevationRepairState backState, - Vertex vertex, double distance, double initialElevation) { + Vertex vertex, double distance, double initialElevation) { this.backEdge = backEdge; this.backState = backState; this.vertex = vertex; @@ -331,7 +339,7 @@ private void assignMissingElevations( if (!elevations.containsKey(e.getFromVertex())) { double firstElevation = profile.getOrdinate(0, 1); ElevationRepairState state = new ElevationRepairState(null, null, - e.getFromVertex(), 0, firstElevation); + e.getFromVertex(), 0, firstElevation); pq.insert(state, 0); elevations.put(e.getFromVertex(), firstElevation); } @@ -339,7 +347,7 @@ private void assignMissingElevations( if (!elevations.containsKey(e.getToVertex())) { double lastElevation = profile.getOrdinate(profile.size() - 1, 1); ElevationRepairState state = new ElevationRepairState(null, null, e.getToVertex(), - 0, lastElevation); + 0, lastElevation); pq.insert(state, 0); elevations.put(e.getToVertex(), lastElevation); } @@ -382,7 +390,7 @@ private void assignMissingElevations( } else { // continue ElevationRepairState newState = new ElevationRepairState(edge, state, tov, - e.getDistance() + state.distance, state.initialElevation); + e.getDistance() + state.distance, state.initialElevation); pq.insert(newState, e.getDistance() + state.distance); } } // end loop over outgoing edges @@ -405,7 +413,7 @@ private void assignMissingElevations( } else { // continue ElevationRepairState newState = new ElevationRepairState(edge, state, fromv, - e.getDistance() + state.distance, state.initialElevation); + e.getDistance() + state.distance, state.initialElevation); pq.insert(newState, e.getDistance() + state.distance); } } // end loop over incoming edges @@ -423,13 +431,13 @@ private void assignMissingElevations( double totalDistance = bestDistance + state.distance; // trace backwards, setting states as we go while (true) { - // watch out for division by 0 here, which will propagate NaNs - // all the way out to edge lengths + // watch out for division by 0 here, which will propagate NaNs + // all the way out to edge lengths if (totalDistance == 0) elevations.put(state.vertex, bestElevation); else { - double elevation = (bestElevation * state.distance + - state.initialElevation * bestDistance) / totalDistance; + double elevation = (bestElevation * state.distance + + state.initialElevation * bestDistance) / totalDistance; elevations.put(state.vertex, elevation); } if (state.backState == null) @@ -559,7 +567,7 @@ private void processEdge(StreetWithElevationEdge ee, Coverage coverage) { // construct the PCS Coordinate coordArr[] = new Coordinate[coordList.size()]; PackedCoordinateSequence elevPCS = new PackedCoordinateSequence.Double( - coordList.toArray(coordArr)); + coordList.toArray(coordArr)); setEdgeElevationProfile(ee, elevPCS, graph); } catch (PointOutsideCoverageException | TransformException e) { @@ -608,7 +616,9 @@ private double getElevation(Coverage coverage, double x, double y) throws PointO throw e; } nPointsEvaluated.incrementAndGet(); - return values[0] - (includeEllipsoidToGeoidDifference ? getApproximateEllipsoidToGeoidDifference(y, x) : 0); + return values[0] * elevationUnitMultiplier - ( + includeEllipsoidToGeoidDifference ? getApproximateEllipsoidToGeoidDifference(y, x) : 0 + ); } /** diff --git a/src/main/java/org/opentripplanner/graph_builder/module/ned/NEDDownloader.java b/src/main/java/org/opentripplanner/graph_builder/module/ned/NEDDownloader.java index e7f4a6b066d..6038156ec10 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/ned/NEDDownloader.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/ned/NEDDownloader.java @@ -1,6 +1,6 @@ package org.opentripplanner.graph_builder.module.ned; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; import org.apache.axis.client.Call; import org.apache.axis.client.Service; import org.opentripplanner.graph_builder.services.ned.NEDTileSource; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/ned/UnifiedGridCoverage.java b/src/main/java/org/opentripplanner/graph_builder/module/ned/UnifiedGridCoverage.java index 0a14b47b76a..1756fa35be6 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/ned/UnifiedGridCoverage.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/ned/UnifiedGridCoverage.java @@ -1,12 +1,12 @@ package org.opentripplanner.graph_builder.module.ned; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.index.SpatialIndex; -import com.vividsolutions.jts.index.strtree.STRtree; import org.geotools.coverage.AbstractCoverage; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.geometry.jts.ReferencedEnvelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.SpatialIndex; +import org.locationtech.jts.index.strtree.STRtree; import org.opengis.coverage.CannotEvaluateException; import org.opengis.coverage.Coverage; import org.opengis.coverage.PointOutsideCoverageException; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java index 34ad7c74209..52c4df8deb2 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java @@ -12,8 +12,8 @@ import org.opentripplanner.openstreetmap.model.OSMWithTags; import com.google.common.collect.ArrayListMultimap; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; /** * Stores information about an OSM area needed for visibility graph construction. Algorithm based on diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaGroup.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaGroup.java index 5f10941ebfd..f732562bdd2 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaGroup.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaGroup.java @@ -19,12 +19,12 @@ import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryCollection; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Polygon; /** * A group of possibly-contiguous areas sharing the same level diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMDatabase.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMDatabase.java index a04ba7bd135..d229cb43f6b 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMDatabase.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMDatabase.java @@ -3,7 +3,7 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; -import com.vividsolutions.jts.geom.*; +import org.locationtech.jts.geom.*; import org.opentripplanner.common.RepeatingTimePeriod; import org.opentripplanner.common.TurnRestrictionType; import org.opentripplanner.common.geometry.GeometryUtils; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMFilter.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMFilter.java index 47a9d0f7578..123abbb4794 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMFilter.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OSMFilter.java @@ -26,23 +26,30 @@ public class OSMFilter { * (as well as ways where all access is specifically forbidden to the public). * http://wiki.openstreetmap.org/wiki/Tag:highway%3Dproposed */ - public static boolean isWayRoutable(OSMWithTags way) { - if (!isOsmEntityRoutable(way)) + static boolean isWayRoutable(OSMWithTags way) { + if (!isOsmEntityRoutable(way)) { return false; + } String highway = way.getTag("highway"); - if (highway != null - && (highway.equals("conveyer") || highway.equals("proposed") - || highway.equals("construction") || highway.equals("raceway") || highway - .equals("unbuilt"))) - return false; + if (highway != null) { + if( + highway.equals("conveyer") || + highway.equals("proposed") || + highway.equals("construction") || + highway.equals("razed") || + highway.equals("raceway") || + highway.equals("unbuilt") + ) { + return false; + } + } if (way.isGeneralAccessDenied()) { // There are exceptions. return (way.isMotorcarExplicitlyAllowed() || way.isBicycleExplicitlyAllowed() || way .isPedestrianExplicitlyAllowed() || way.isMotorVehicleExplicitlyAllowed()); } - return true; } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java index 9bf84ab3bbd..6856fdc78ab 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java @@ -3,12 +3,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Iterables; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.prep.PreparedGeometry; -import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; import org.opentripplanner.analyst.UnsupportedGeometryException; import org.opentripplanner.common.TurnRestriction; import org.opentripplanner.common.geometry.GeometryUtils; @@ -244,7 +244,7 @@ public void loadMicromobilityTravelRestrictions(GraphBuilderParameters params) { * value is returned if that operation was unsuccessful in any way. */ private PreparedGeometry parseGeometry(String areaUrlOrFile) { - if (areaUrlOrFile == null) { + if (areaUrlOrFile == null || areaUrlOrFile.equals("")) { return null; } InputStream data = null; @@ -307,7 +307,7 @@ public void buildGraph() { } for (Area area : Iterables.concat(osmdb.getWalkableAreas(), - osmdb.getParkAndRideAreas(), osmdb.getBikeParkingAreas())) + osmdb.getParkAndRideAreas(), osmdb.getBikeParkingAreas())) setWayName(area.parent); // figure out which nodes that are actually intersections @@ -350,7 +350,7 @@ private void processBikeRentalNodes() { LOG.info("Processing bike rental nodes..."); int n = 0; BikeRentalStationService bikeRentalService = graph.getService( - BikeRentalStationService.class, true); + BikeRentalStationService.class, true); graph.putService(BikeRentalStationService.class, bikeRentalService); for (OSMNode node : osmdb.getBikeRentalNodes()) { n++; @@ -364,7 +364,7 @@ private void processBikeRentalNodes() { capacity = node.getCapacity(); } catch (NumberFormatException e) { LOG.warn("Capacity for osm node " + node.getId() + " (" + creativeName - + ") is not a number: " + node.getTag("capacity")); + + ") is not a number: " + node.getTag("capacity")); } } String networks = node.getTag("network"); @@ -376,7 +376,7 @@ private void processBikeRentalNodes() { networkSet.addAll(Arrays.asList(operators.split(";"))); if (networkSet.isEmpty()) { LOG.warn("Bike rental station at osm node " + node.getId() + " (" - + creativeName + ") with no network; including as compatible-with-all."); + + creativeName + ") with no network; including as compatible-with-all."); networkSet = null; // Special "catch-all" value } BikeRentalStation station = new BikeRentalStation(); @@ -405,7 +405,7 @@ private void processBikeParkAndRideNodes() { LOG.info("Processing bike P+R nodes..."); int n = 0; BikeRentalStationService bikeRentalService = graph.getService( - BikeRentalStationService.class, true); + BikeRentalStationService.class, true); for (OSMNode node : osmdb.getBikeParkingNodes()) { n++; I18NString creativeName = wayPropertySet.getCreativeNameForWay(node); @@ -453,7 +453,7 @@ private void buildBikeParkAndRideAreas() { */ private void buildBikeParkAndRideForArea(Area area) { BikeRentalStationService bikeRentalService = graph.getService( - BikeRentalStationService.class, true); + BikeRentalStationService.class, true); Envelope envelope = new Envelope(); long osmId = area.parent.getId(); I18NString creativeName = wayPropertySet.getCreativeNameForWay(area.parent); @@ -482,7 +482,7 @@ private void buildWalkableAreas(boolean skipVisibility, boolean platformEntriesL } List areaGroups = groupAreas(osmdb.getWalkableAreas()); WalkableAreaBuilder walkableAreaBuilder = new WalkableAreaBuilder(graph, osmdb, - wayPropertySet, edgeFactory, this); + wayPropertySet, edgeFactory, this); if (skipVisibility) { for (AreaGroup group : areaGroups) { walkableAreaBuilder.buildWithoutVisibility(group); @@ -494,8 +494,8 @@ private void buildWalkableAreas(boolean skipVisibility, boolean platformEntriesL if(platformEntriesLinking){ List platforms = osmdb.getWalkableAreas().stream(). - filter(area -> "platform".equals(area.parent.getTag("public_transport"))). - collect(Collectors.toList()); + filter(area -> "platform".equals(area.parent.getTag("public_transport"))). + collect(Collectors.toList()); List platformGroups = groupAreas(platforms); for (AreaGroup group : platformGroups) { walkableAreaBuilder.buildWithoutVisibility(group); @@ -543,7 +543,7 @@ private boolean buildParkAndRideAreasForGroup(AreaGroup group) { envelope.expandToInclude(new Coordinate(node.lon, node.lat)); OsmVertex accessVertex = getVertexForOsmNode(node, area.parent); if (accessVertex.getIncoming().isEmpty() - || accessVertex.getOutgoing().isEmpty()) + || accessVertex.getOutgoing().isEmpty()) continue; accessVertexes.add(accessVertex); } @@ -551,9 +551,9 @@ private boolean buildParkAndRideAreasForGroup(AreaGroup group) { } // Check P+R accessibility by walking and driving. TraversalRequirements walkReq = new TraversalRequirements(new RoutingRequest( - TraverseMode.WALK)); + TraverseMode.WALK)); TraversalRequirements driveReq = new TraversalRequirements(new RoutingRequest( - TraverseMode.CAR)); + TraverseMode.CAR)); boolean walkAccessibleIn = false; boolean carAccessibleIn = false; boolean walkAccessibleOut = false; @@ -591,8 +591,8 @@ private boolean buildParkAndRideAreasForGroup(AreaGroup group) { } // Place the P+R at the center of the envelope ParkAndRideVertex parkAndRideVertex = new ParkAndRideVertex(graph, "P+R" + osmId, - "P+R_" + osmId, (envelope.getMinX() + envelope.getMaxX()) / 2, - (envelope.getMinY() + envelope.getMaxY()) / 2, creativeName); + "P+R_" + osmId, (envelope.getMinX() + envelope.getMaxX()) / 2, + (envelope.getMinY() + envelope.getMaxY()) / 2, creativeName); new ParkAndRideEdge(parkAndRideVertex); for (OsmVertex accessVertex : accessVertexes) { new ParkAndRideLinkEdge(parkAndRideVertex, accessVertex); @@ -628,7 +628,7 @@ private void buildBasicGraph() { setWayName(way); StreetTraversalPermission permissions = OSMFilter.getPermissionsForWay(way, - wayData.getPermission(), graph, banDiscouragedWalking, banDiscouragedBiking); + wayData.getPermission(), graph, banDiscouragedWalking, banDiscouragedBiking); if (!OSMFilter.isWayRoutable(way) || permissions.allowsNothing()) continue; @@ -654,7 +654,7 @@ private void buildBasicGraph() { } } if (nodeId != last - && (node.lat != lastLat || node.lon != lastLon || levelsDiffer)) + && (node.lat != lastLat || node.lon != lastLon || levelsDiffer)) nodes.add(nodeId); last = nodeId; lastLon = node.lon; @@ -704,14 +704,14 @@ private void buildBasicGraph() { } if (intersectionNodes.containsKey(endNode) || i == nodes.size() - 2 - || nodes.subList(0, i).contains(nodes.get(i)) - || osmEndNode.hasTag("ele") - || osmEndNode.isStop() - || osmEndNode.isBollard()) { + || nodes.subList(0, i).contains(nodes.get(i)) + || osmEndNode.hasTag("ele") + || osmEndNode.isStop() + || osmEndNode.isBollard()) { segmentCoordinates.add(getCoordinate(osmEndNode)); geometry = GeometryUtils.getGeometryFactory().createLineString( - segmentCoordinates.toArray(new Coordinate[0])); + segmentCoordinates.toArray(new Coordinate[0])); segmentCoordinates.clear(); } else { segmentCoordinates.add(getCoordinate(osmEndNode)); @@ -743,7 +743,7 @@ private void buildBasicGraph() { } } P2 streets = getEdgesForStreet(startEndpoint, endEndpoint, - way, i, osmStartNode.getId(), osmEndNode.getId(), permissions, geometry); + way, i, osmStartNode.getId(), osmEndNode.getId(), permissions, geometry); StreetEdge street = streets.first; StreetEdge backStreet = streets.second; @@ -795,7 +795,7 @@ private void applyMicromobilityRestrictions(StreetEdge streetEdge) { // TODO Set this to private once WalkableAreaBuilder is gone protected void applyWayProperties(StreetEdge street, StreetEdge backStreet, - WayProperties wayData, OSMWithTags way) { + WayProperties wayData, OSMWithTags way) { Set> notes = wayPropertySet.getNoteForWay(way); float walkSafetyFactor = walkLTSGenerator.computeScore(way); @@ -870,11 +870,11 @@ private void buildElevatorEdges(Graph graph) { /* * first, build FreeEdges to disconnect from the graph, GenericVertices to serve as attachment points, and ElevatorBoard and * ElevatorAlight edges to connect future ElevatorHop edges to. After this iteration, graph will look like (side view): +==+~~X - * + * * +==+~~X - * + * * +==+~~X - * + * * + GenericVertex, X EndpointVertex, ~~ FreeEdge, == ElevatorBoardEdge/ElevatorAlightEdge Another loop will fill in the * ElevatorHopEdges. */ @@ -889,15 +889,15 @@ private void buildElevatorEdges(Graph graph) { String levelName = level.longName; ElevatorOffboardVertex offboardVertex = new ElevatorOffboardVertex(graph, - sourceVertexLabel + "_offboard", sourceVertex.getX(), - sourceVertex.getY(), levelName); + sourceVertexLabel + "_offboard", sourceVertex.getX(), + sourceVertex.getY(), levelName); new FreeEdge(sourceVertex, offboardVertex); new FreeEdge(offboardVertex, sourceVertex); ElevatorOnboardVertex onboardVertex = new ElevatorOnboardVertex(graph, - sourceVertexLabel + "_onboard", sourceVertex.getX(), - sourceVertex.getY(), levelName); + sourceVertexLabel + "_onboard", sourceVertex.getX(), + sourceVertex.getY(), levelName); new ElevatorBoardEdge(offboardVertex, onboardVertex); new ElevatorAlightEdge(onboardVertex, offboardVertex, level.longName); @@ -1016,7 +1016,7 @@ private void unifyTurnRestrictions() { } private void applyEdgesToTurnRestrictions(OSMWay way, long startNode, long endNode, - StreetEdge street, StreetEdge backStreet) { + StreetEdge street, StreetEdge backStreet) { /* Check if there are turn restrictions starting on this segment */ Collection restrictionTags = osmdb.getFromWayTurnRestrictions(way.getId()); @@ -1079,7 +1079,7 @@ private void initIntersectionNodes() { */ private void applyBikeSafetyFactor(Graph graph) { LOG.info(graph.addBuilderAnnotation(new Graphwide( - "Multiplying all bike safety values by " + (1 / bestBikeSafety)))); + "Multiplying all bike safety values by " + (1 / bestBikeSafety)))); HashSet seenEdges = new HashSet(); HashSet seenAreas = new HashSet(); for (Vertex vertex : graph.getVertices()) { @@ -1091,7 +1091,7 @@ private void applyBikeSafetyFactor(Graph graph) { seenAreas.add(areaEdgeList); for (NamedArea area : areaEdgeList.getAreas()) { area.setBicycleSafetyMultiplier(area.getBicycleSafetyMultiplier() - / bestBikeSafety); + / bestBikeSafety); } } if (!(e instanceof StreetEdge)) { @@ -1153,8 +1153,8 @@ private double getGeometryLengthMeters(Geometry geometry) { * @param startEndpoint */ private P2 getEdgesForStreet(OsmVertex startEndpoint, - OsmVertex endEndpoint, OSMWay way, int index, long startNode, long endNode, - StreetTraversalPermission permissions, LineString geometry) { + OsmVertex endEndpoint, OSMWay way, int index, long startNode, long endNode, + StreetTraversalPermission permissions, LineString geometry) { // No point in returning edges that can't be traversed by anyone. if (permissions.allowsNothing()) { return new P2(null, null); @@ -1165,17 +1165,17 @@ private P2 getEdgesForStreet(OsmVertex startEndpoint, double length = this.getGeometryLengthMeters(geometry); P2 permissionPair = OSMFilter.getPermissions(permissions, - way); + way); StreetTraversalPermission permissionsFront = permissionPair.first; StreetTraversalPermission permissionsBack = permissionPair.second; if (permissionsFront.allowsAnything()) { street = getEdgeForStreet(startEndpoint, endEndpoint, way, index, startNode, endNode, length, - permissionsFront, geometry, false); + permissionsFront, geometry, false); } if (permissionsBack.allowsAnything()) { backStreet = getEdgeForStreet(endEndpoint, startEndpoint, way, index, endNode, startNode, length, - permissionsBack, backGeometry, true); + permissionsBack, backGeometry, true); } if (street != null && backStreet != null) { backStreet.shareData(street); @@ -1193,8 +1193,8 @@ private P2 getEdgesForStreet(OsmVertex startEndpoint, } private StreetEdge getEdgeForStreet(OsmVertex startEndpoint, OsmVertex endEndpoint, - OSMWay way, int index, long startNode, long endNode, double length, - StreetTraversalPermission permissions, LineString geometry, boolean back) { + OSMWay way, int index, long startNode, long endNode, double length, + StreetTraversalPermission permissions, LineString geometry, boolean back) { String label = "way " + way.getId() + " from " + index; label = unique(label); @@ -1209,7 +1209,7 @@ private StreetEdge getEdgeForStreet(OsmVertex startEndpoint, OsmVertex endEndpoi float carSpeed = wayPropertySet.getCarSpeedForWay(way, back); StreetEdge street = edgeFactory.createEdge(startEndpoint, endEndpoint, geometry, name, length, - permissions, back); + permissions, back); street.setCarSpeed(carSpeed); street.setTNCStopSuitability(wayPropertySet.isSuitableForTNCStop(way)); street.setFloatingCarDropoffSuitability(wayPropertySet.isSuitableForStreetParking(way)); @@ -1219,12 +1219,12 @@ private StreetEdge getEdgeForStreet(OsmVertex startEndpoint, OsmVertex endEndpoi if ("crossing".equals(highway) && !way.isTag("bicycle", "designated")) { cls = StreetEdge.CLASS_CROSSING; } else if ("footway".equals(highway) && way.isTag("footway", "crossing") - && !way.isTag("bicycle", "designated")) { + && !way.isTag("bicycle", "designated")) { cls = StreetEdge.CLASS_CROSSING; } else if ("residential".equals(highway) || "tertiary".equals(highway) - || "secondary".equals(highway) || "secondary_link".equals(highway) - || "primary".equals(highway) || "primary_link".equals(highway) - || "trunk".equals(highway) || "trunk_link".equals(highway)) { + || "secondary".equals(highway) || "secondary_link".equals(highway) + || "primary".equals(highway) || "primary_link".equals(highway) + || "trunk".equals(highway) || "trunk_link".equals(highway)) { cls = StreetEdge.CLASS_STREET; } else { cls = StreetEdge.CLASS_OTHERPATH; @@ -1240,7 +1240,7 @@ private StreetEdge getEdgeForStreet(OsmVertex startEndpoint, OsmVertex endEndpoi /* TODO: This should probably generalized somehow? */ if (!ignoreWheelchairAccessibility - && (way.isTagFalse("wheelchair") || (steps && !way.isTagTrue("wheelchair")))) { + && (way.isTagFalse("wheelchair") || (steps && !way.isTagTrue("wheelchair")))) { street.setWheelchairAccessible(false); } @@ -1296,7 +1296,7 @@ private OsmVertex recordLevel(OSMNode node, OSMWithTags way) { Coordinate coordinate = getCoordinate(node); String label = this.getLevelNodeLabel(node, level); OsmVertex vertex = new OsmVertex(graph, label, coordinate.x, - coordinate.y, node.getId(), new NonLocalizedString(label)); + coordinate.y, node.getId(), new NonLocalizedString(label)); vertices.put(level, vertex); // multilevel nodes should also undergo turn-conversion endpoints.add(vertex); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/PlatformLinker.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/PlatformLinker.java index 756c9bf3ca8..060eb7c19ee 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/PlatformLinker.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/PlatformLinker.java @@ -1,8 +1,8 @@ package org.opentripplanner.graph_builder.module.osm; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.graph_builder.services.StreetEdgeFactory; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java index 39765b6b43c..b27be031a13 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java @@ -10,12 +10,12 @@ import org.opentripplanner.visibility.VLPoint; import org.opentripplanner.visibility.VLPolygon; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; public class Ring { diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/UKWayPropertySetSource.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/UKWayPropertySetSource.java new file mode 100644 index 00000000000..aa11454c710 --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/UKWayPropertySetSource.java @@ -0,0 +1,72 @@ +package org.opentripplanner.graph_builder.module.osm; + +import org.opentripplanner.routing.edgetype.StreetTraversalPermission; + +/** + * OSM way properties for UK roads. + * The main differences compared to the default property set are: + * 1. In the UK there is no real distinction between trunk highways and primary highways, other than the + * body responsible for them. Most highway=trunk and highway=trunk_link will allow traversal by all modes. + * 2. Speeds have been set to reflect average free flow road speeds provided by UK DfT. In particular + * note that a distinction is made between tertiary and unclassified/residential roads. The default has these + * the same (25mph) but in the UK tertiary roads are considered by OSM tagging guidelines to be busy unclassified + * through roads wide enough to allow two cars to pass safely. The free flow speeds are therefore higher. + * These changes result in more realistic driving routes. + * https://www.gov.uk/government/statistical-data-sets/vehicle-speed-compliance-statistics-data-tables-spe + * https://wiki.openstreetmap.org/wiki/United_Kingdom_Tagging_Guidelines + * + * @author marcusyoung + * @see WayPropertySetSource + * @see DefaultWayPropertySetSource + */ +public class UKWayPropertySetSource implements WayPropertySetSource { + + @Override + public void populateProperties(WayPropertySet props) { + // Replace existing matching properties as the logic is that the first statement registered takes precedence over later statements + props.setProperties("highway=trunk_link", StreetTraversalPermission.ALL, 2.06, 2.06); + props.setProperties("highway=trunk", StreetTraversalPermission.ALL, 7.47, 7.47); + props.setProperties("highway=trunk;cycleway=lane", StreetTraversalPermission.ALL, 1.5, + 1.5); + props.setProperties("highway=trunk_link;cycleway=lane", StreetTraversalPermission.ALL, + 1.15, 1.15); + props.setProperties("highway=trunk;cycleway=share_busway", StreetTraversalPermission.ALL, + 1.75, 1.75); + props.setProperties("highway=trunk_link;cycleway=share_busway", + StreetTraversalPermission.ALL,1.25, 1.25); + props.setProperties("highway=trunk;cycleway=opposite_lane", StreetTraversalPermission.ALL, + 7.47, 1.5); + props.setProperties("highway=trunk_link;cycleway=opposite_lane", + StreetTraversalPermission.ALL, 2.06, 1.15); + props.setProperties("highway=trunk;cycleway=track", StreetTraversalPermission.ALL, 0.95, + 0.95); + props.setProperties("highway=trunk_link;cycleway=track", StreetTraversalPermission.ALL, + 0.85, 0.85); + props.setProperties("highway=trunk;cycleway=opposite_track", StreetTraversalPermission.ALL, + 7.47, 0.95); + props.setProperties("highway=trunk_link;cycleway=opposite_track", + StreetTraversalPermission.ALL, 2.06, 0.85); + props.setProperties("highway=trunk;bicycle=designated", StreetTraversalPermission.ALL, + 7.25, 7.25); + props.setProperties("highway=trunk_link;bicycle=designated", StreetTraversalPermission.ALL, + 2, 2); + + /* + * Automobile speeds in UK. Based on recorded free flow speeds for motorways, trunk and primary and + * my (marcusyoung) personal experience in obtaining realistic routes. + * + */ + props.setCarSpeed("highway=motorway", 30.4f); // ~=68mph + props.setCarSpeed("highway=motorway_link", 22.4f); // ~= 50mph + props.setCarSpeed("highway=trunk", 22.4f); // ~=50mph + props.setCarSpeed("highway=trunk_link", 17.9f); // ~= 40mph + props.setCarSpeed("highway=primary", 22.4f); // ~=50mph + props.setCarSpeed("highway=primary_link", 17.9f); // ~= 40mph + props.setCarSpeed("highway=secondary", 17.9f); // ~= 40mph + props.setCarSpeed("highway=secondary_link", 13.4f); // ~= 30mph + props.setCarSpeed("highway=tertiary", 15.7f); // ~= 35mph + + // Read the rest from the default set + new DefaultWayPropertySetSource().populateProperties(props); + } +} diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java index 12ae3cccf6e..998d0d2fe69 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java @@ -39,15 +39,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.MultiLineString; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.opentripplanner.util.I18NString; /** diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/WayPropertySetSource.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/WayPropertySetSource.java index 3068b71fbcd..8e2adad4d93 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/WayPropertySetSource.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/WayPropertySetSource.java @@ -21,6 +21,8 @@ public static WayPropertySetSource fromConfig(String type) { return new DefaultWayPropertySetSource(); } else if ("norway".equals(type)) { return new NorwayWayPropertySetSource(); + } else if ("uk".equals(type)) { + return new UKWayPropertySetSource(); } else { throw new IllegalArgumentException(String.format("Unknown osmWayPropertySet: '%s'", type)); } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/shapefile/ShapefileStreetModule.java b/src/main/java/org/opentripplanner/graph_builder/module/shapefile/ShapefileStreetModule.java index 50dbf52a0ad..abadb63116a 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/shapefile/ShapefileStreetModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/shapefile/ShapefileStreetModule.java @@ -38,10 +38,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.MultiLineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; import org.opentripplanner.util.NonLocalizedString; /** diff --git a/src/main/java/org/opentripplanner/graph_builder/services/DefaultStreetEdgeFactory.java b/src/main/java/org/opentripplanner/graph_builder/services/DefaultStreetEdgeFactory.java index 96b6d677d42..363b996b9d1 100644 --- a/src/main/java/org/opentripplanner/graph_builder/services/DefaultStreetEdgeFactory.java +++ b/src/main/java/org/opentripplanner/graph_builder/services/DefaultStreetEdgeFactory.java @@ -7,7 +7,7 @@ import org.opentripplanner.routing.edgetype.StreetWithElevationEdge; import org.opentripplanner.routing.vertextype.IntersectionVertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.util.I18NString; public class DefaultStreetEdgeFactory implements StreetEdgeFactory { diff --git a/src/main/java/org/opentripplanner/graph_builder/services/StreetEdgeFactory.java b/src/main/java/org/opentripplanner/graph_builder/services/StreetEdgeFactory.java index 6e2723e5d3a..13238de574b 100644 --- a/src/main/java/org/opentripplanner/graph_builder/services/StreetEdgeFactory.java +++ b/src/main/java/org/opentripplanner/graph_builder/services/StreetEdgeFactory.java @@ -6,7 +6,7 @@ import org.opentripplanner.routing.edgetype.StreetTraversalPermission; import org.opentripplanner.routing.vertextype.IntersectionVertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.util.I18NString; /** diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/AreaMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/AreaMapper.java new file mode 100644 index 00000000000..dc79f9f49f3 --- /dev/null +++ b/src/main/java/org/opentripplanner/gtfs/mapping/AreaMapper.java @@ -0,0 +1,32 @@ +package org.opentripplanner.gtfs.mapping; + +import org.opentripplanner.model.FlexArea; +import org.opentripplanner.util.MapUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** Map from the OBA model of GTFS-flex areas to the OTP internal model of areas. */ +class AreaMapper { + + private final Map mappedAreas = new HashMap<>(); + + Collection map(Collection agencies) { + return MapUtils.mapToList(agencies, this::map); + } + + /** Map from the OBA model of GTFS-flex areas to the OTP internal model of areas. */ + FlexArea map(org.onebusaway.gtfs.model.Area orginal) { + return orginal == null ? null : mappedAreas.computeIfAbsent(orginal, this::doMap); + } + + private FlexArea doMap(org.onebusaway.gtfs.model.Area rhs) { + FlexArea lhs = new FlexArea(); + + lhs.setId(AgencyAndIdMapper.mapAgencyAndId(rhs.getId())); + lhs.setWkt(rhs.getWkt()); + + return lhs; + } +} diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java index 2805052595e..92f1f208bb5 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java @@ -42,6 +42,8 @@ public class GTFSToOtpTransitServiceMapper { routeMapper, fareAttributeMapper ); + private final AreaMapper areaMapper = new AreaMapper(); + /** * Map from GTFS data to the internal OTP model */ @@ -66,6 +68,7 @@ private OtpTransitService map(org.onebusaway.gtfs.services.GtfsRelationalDao dat builder.getStopTimes().addAll(stopTimeMapper.map(data.getAllStopTimes())); builder.getTransfers().addAll(transferMapper.map(data.getAllTransfers())); builder.getTrips().addAll(tripMapper.map(data.getAllTrips())); + builder.getFlexAreas().addAll(areaMapper.map(data.getAllAreas())); return builder.build(); } diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/RouteMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/RouteMapper.java index a9108fd2818..bef952e95c7 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/RouteMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/RouteMapper.java @@ -42,6 +42,7 @@ private Route doMap(org.onebusaway.gtfs.model.Route rhs) { lhs.setBikesAllowed(rhs.getBikesAllowed()); lhs.setSortOrder(rhs.getSortOrder()); lhs.setBrandingUrl(rhs.getBrandingUrl()); + lhs.setEligibilityRestricted(rhs.getEligibilityRestricted()); return lhs; } diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java index 82412c33519..ab592f9069d 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java @@ -44,6 +44,12 @@ private StopTime doMap(org.onebusaway.gtfs.model.StopTime rhs) { lhs.setDropOffType(rhs.getDropOffType()); lhs.setShapeDistTraveled(rhs.getShapeDistTraveled()); lhs.setFarePeriodId(rhs.getFarePeriodId()); + lhs.setContinuousPickup(rhs.getContinuousPickup()); + lhs.setContinuousDropOff(rhs.getContinuousDropOff()); + lhs.setStartServiceArea(rhs.getStartServiceArea()); + lhs.setEndServiceArea(rhs.getEndServiceArea()); + lhs.setStartServiceAreaRadius(rhs.getStartServiceAreaRadius()); + lhs.setEndServiceAreaRadius(rhs.getEndServiceAreaRadius()); // Skip mapping of proxy // private transient StopTimeProxy proxy; diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java index 848ebbdd58d..41ce89d9b35 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java @@ -42,6 +42,13 @@ private Trip doMap(org.onebusaway.gtfs.model.Trip rhs) { lhs.setTripBikesAllowed(rhs.getTripBikesAllowed()); lhs.setBikesAllowed(rhs.getBikesAllowed()); lhs.setFareId(rhs.getFareId()); + lhs.setDrtMaxTravelTime(rhs.getDrtMaxTravelTime()); + lhs.setDrtAvgTravelTime(rhs.getDrtAvgTravelTime()); + lhs.setDrtAdvanceBookMin(rhs.getDrtAdvanceBookMin()); + lhs.setDrtPickupMessage(rhs.getDrtPickupMessage()); + lhs.setDrtDropOffMessage(rhs.getDrtDropOffMessage()); + lhs.setContinuousPickupMessage(rhs.getContinuousPickupMessage()); + lhs.setContinuousDropOffMessage(rhs.getContinuousDropOffMessage()); return lhs; } diff --git a/src/main/java/org/opentripplanner/index/IndexAPI.java b/src/main/java/org/opentripplanner/index/IndexAPI.java index 81343c806fd..236c1b42b75 100644 --- a/src/main/java/org/opentripplanner/index/IndexAPI.java +++ b/src/main/java/org/opentripplanner/index/IndexAPI.java @@ -6,8 +6,9 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; import org.opentripplanner.model.Agency; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.FeedInfo; @@ -17,6 +18,7 @@ import org.opentripplanner.model.calendar.ServiceDate; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.gtfs.GtfsLibrary; +import org.opentripplanner.index.model.AreaShort; import org.opentripplanner.index.model.PatternDetail; import org.opentripplanner.index.model.PatternShort; import org.opentripplanner.index.model.RouteShort; @@ -599,6 +601,29 @@ public Response getStopCluster (@PathParam("clusterId") String clusterIdString) } } + /** + * Return all GTFS-Flex area IDs. Areas are defined in GTFS-Flex to be a lat/lon polygon in + * which certain kinds of flex service take place (deviated-route and call-and-ride). + */ + @GET + @Path("/flexAreas") + public Response getAllFlexAreas() { + List ids = new ArrayList<>(index.flexAreasById.keySet()); + return Response.status(Status.OK).entity(ids).build(); + } + + /** Return a specific GTFS-Flex area given an ID in Agency:ID format. */ + @GET + @Path("/flexAreas/{id}") + public Response getFlexAreaByFeedId(@PathParam("id") String areaIdString) { + FeedScopedId id = GtfsLibrary.convertIdFromString(areaIdString); + Geometry area = index.flexAreasById.get(id); + if (area == null) { + return Response.status(Status.NOT_FOUND).build(); + } + return Response.status(Status.OK).entity(new AreaShort(id, area)).build(); + } + @POST @Path("/graphql") @Consumes(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/opentripplanner/index/IndexGraphQLSchema.java b/src/main/java/org/opentripplanner/index/IndexGraphQLSchema.java index b9c1900b469..fd7bbcac4be 100644 --- a/src/main/java/org/opentripplanner/index/IndexGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/index/IndexGraphQLSchema.java @@ -1,9 +1,9 @@ package org.opentripplanner.index; import com.google.common.collect.ImmutableMap; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineString; import graphql.Scalars; import graphql.relay.Relay; import graphql.relay.SimpleListConnection; diff --git a/src/main/java/org/opentripplanner/index/model/AreaShort.java b/src/main/java/org/opentripplanner/index/model/AreaShort.java new file mode 100644 index 00000000000..39bc530c18d --- /dev/null +++ b/src/main/java/org/opentripplanner/index/model/AreaShort.java @@ -0,0 +1,16 @@ +package org.opentripplanner.index.model; + +import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.model.FeedScopedId; +import org.opentripplanner.util.PolylineEncoder; +import org.opentripplanner.util.model.EncodedPolylineBean; + +public class AreaShort { + public FeedScopedId areaId; + public EncodedPolylineBean polygon; + + public AreaShort(FeedScopedId areaId, Geometry polygon) { + this.areaId = areaId; + this.polygon = PolylineEncoder.createEncodings(polygon.getBoundary()); + } +} diff --git a/src/main/java/org/opentripplanner/index/model/TripTimeShort.java b/src/main/java/org/opentripplanner/index/model/TripTimeShort.java index 8b030343c2a..a5e1754da43 100644 --- a/src/main/java/org/opentripplanner/index/model/TripTimeShort.java +++ b/src/main/java/org/opentripplanner/index/model/TripTimeShort.java @@ -31,6 +31,10 @@ public class TripTimeShort { public FeedScopedId tripId; public String blockId; public String headsign; + public int continuousPickup; + public int continuousDropOff; + public double serviceAreaRadius; + public String serviceArea; /** * This is stop-specific, so the index i is a stop index, not a hop index. @@ -50,6 +54,10 @@ public TripTimeShort(TripTimes tt, int i, Stop stop) { realtimeState = tt.getRealTimeState(); blockId = tt.trip.getBlockId(); headsign = tt.getHeadsign(i); + continuousPickup = tt.getContinuousPickup(i); + continuousDropOff = tt.getContinuousDropOff(i); + serviceAreaRadius = tt.getServiceAreaRadius(i); + serviceArea = tt.getServiceArea(i); } public TripTimeShort(TripTimes tt, int i, Stop stop, ServiceDay sd) { diff --git a/src/main/java/org/opentripplanner/inspector/EdgeVertexTileRenderer.java b/src/main/java/org/opentripplanner/inspector/EdgeVertexTileRenderer.java index 06732e1757e..414aec4040b 100644 --- a/src/main/java/org/opentripplanner/inspector/EdgeVertexTileRenderer.java +++ b/src/main/java/org/opentripplanner/inspector/EdgeVertexTileRenderer.java @@ -19,18 +19,18 @@ import com.jhlabs.awt.ShapeStroke; import com.jhlabs.awt.TextStroke; -import com.vividsolutions.jts.awt.IdentityPointTransformation; -import com.vividsolutions.jts.awt.PointShapeFactory; -import com.vividsolutions.jts.awt.ShapeWriter; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.PrecisionModel; -import com.vividsolutions.jts.operation.buffer.BufferParameters; -import com.vividsolutions.jts.operation.buffer.OffsetCurveBuilder; +import org.locationtech.jts.awt.IdentityPointTransformation; +import org.locationtech.jts.awt.PointShapeFactory; +import org.locationtech.jts.awt.ShapeWriter; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.operation.buffer.BufferParameters; +import org.locationtech.jts.operation.buffer.OffsetCurveBuilder; /** * A TileRenderer implementation which get all edges/vertex in the bounding box of the tile, and diff --git a/src/main/java/org/opentripplanner/inspector/TileRenderer.java b/src/main/java/org/opentripplanner/inspector/TileRenderer.java index c27649a9ed6..2f529f8dda2 100644 --- a/src/main/java/org/opentripplanner/inspector/TileRenderer.java +++ b/src/main/java/org/opentripplanner/inspector/TileRenderer.java @@ -4,8 +4,8 @@ import org.opentripplanner.routing.graph.Graph; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.util.AffineTransformation; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.util.AffineTransformation; /** * Interface for a slippy map tile renderer. diff --git a/src/main/java/org/opentripplanner/inspector/TileRendererManager.java b/src/main/java/org/opentripplanner/inspector/TileRendererManager.java index c08b7939431..3d27a21cb49 100644 --- a/src/main/java/org/opentripplanner/inspector/TileRendererManager.java +++ b/src/main/java/org/opentripplanner/inspector/TileRendererManager.java @@ -13,8 +13,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.util.AffineTransformation; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.util.AffineTransformation; /** * Process slippy map tile rendering requests. Get the tile renderer for the given layer, setup a diff --git a/src/main/java/org/opentripplanner/internals/AnalysisUtils.java b/src/main/java/org/opentripplanner/internals/AnalysisUtils.java index 27395796c2e..5be2bc5693e 100644 --- a/src/main/java/org/opentripplanner/internals/AnalysisUtils.java +++ b/src/main/java/org/opentripplanner/internals/AnalysisUtils.java @@ -1,11 +1,11 @@ package org.opentripplanner.internals; import com.google.common.collect.LinkedListMultimap; -import com.vividsolutions.jts.algorithm.ConvexHull; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.Point; +import org.locationtech.jts.algorithm.ConvexHull; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; import org.opentripplanner.common.DisjointSet; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.routing.core.RoutingRequest; diff --git a/src/main/java/org/opentripplanner/kryo/HashBiMapSerializer.java b/src/main/java/org/opentripplanner/kryo/HashBiMapSerializer.java new file mode 100644 index 00000000000..0b95c7534aa --- /dev/null +++ b/src/main/java/org/opentripplanner/kryo/HashBiMapSerializer.java @@ -0,0 +1,19 @@ +package org.opentripplanner.kryo; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.serializers.MapSerializer; +import com.google.common.collect.HashBiMap; + +import java.util.Map; + +/** + * Created by abyrd on 2018-10-25 + */ +public class HashBiMapSerializer extends MapSerializer { + + protected Map create(Kryo kryo, Input input, Class type) { + return HashBiMap.create(); + } + +} diff --git a/src/main/java/org/opentripplanner/model/FlexArea.java b/src/main/java/org/opentripplanner/model/FlexArea.java new file mode 100644 index 00000000000..acf4f04bef2 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/FlexArea.java @@ -0,0 +1,41 @@ +/* This file is based on code copied from project OneBusAway, see the LICENSE file for further information. */ +package org.opentripplanner.model; + +public class FlexArea extends IdentityBean { + private static final long serialVersionUID = 1L; + + private FeedScopedId id; + + private String wkt; + + public FlexArea() { + + } + + public FlexArea(FlexArea a) { + this.id = a.id; + this.wkt = a.wkt; + } + + + public String getAreaId() { + return id.getId(); + } + + public FeedScopedId getId() { + return id; + } + + public void setId(FeedScopedId areaId) { + this.id = areaId; + } + + public String getWkt() { + return wkt; + } + + public void setWkt(String wkt) { + this.wkt = wkt; + } + +} diff --git a/src/main/java/org/opentripplanner/model/OtpTransitService.java b/src/main/java/org/opentripplanner/model/OtpTransitService.java index 97fc70167ea..354b541cb5b 100644 --- a/src/main/java/org/opentripplanner/model/OtpTransitService.java +++ b/src/main/java/org/opentripplanner/model/OtpTransitService.java @@ -56,4 +56,6 @@ public interface OtpTransitService { List getTripAgencyIdsReferencingServiceId(FeedScopedId serviceId); List getStopsForStation(Stop station); + + Collection getAllAreas(); } diff --git a/src/main/java/org/opentripplanner/model/Route.java b/src/main/java/org/opentripplanner/model/Route.java index a2c1d1be592..15fac593cc6 100644 --- a/src/main/java/org/opentripplanner/model/Route.java +++ b/src/main/java/org/opentripplanner/model/Route.java @@ -36,6 +36,8 @@ public final class Route extends IdentityBean { private String brandingUrl; + private int eligibilityRestricted = MISSING_VALUE; + public FeedScopedId getId() { return id; } @@ -153,6 +155,18 @@ public void setBrandingUrl(String brandingUrl) { this.brandingUrl = brandingUrl; } + public boolean hasEligibilityRestricted() { + return eligibilityRestricted != MISSING_VALUE; + } + + public int getEligibilityRestricted() { + return eligibilityRestricted; + } + + public void setEligibilityRestricted(int eligibilityRestricted) { + this.eligibilityRestricted = eligibilityRestricted; + } + @Override public String toString() { return ""; diff --git a/src/main/java/org/opentripplanner/model/StopPattern.java b/src/main/java/org/opentripplanner/model/StopPattern.java index 009371ac500..b567adc085c 100644 --- a/src/main/java/org/opentripplanner/model/StopPattern.java +++ b/src/main/java/org/opentripplanner/model/StopPattern.java @@ -3,10 +3,14 @@ import java.io.Serializable; import java.util.Arrays; import java.util.List; +import java.util.function.Function; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.hash.Hasher; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Polygon; +import org.opentripplanner.routing.trippattern.Deduplicator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,12 +54,17 @@ public class StopPattern implements Serializable { public final int[] pickups; public final int[] dropoffs; + /** GTFS-Flex specific fields; will be null unless GTFS-Flex dataset is in use. */ + private StopPatternFlexFields flexFields; + public boolean equals(Object other) { if (other instanceof StopPattern) { StopPattern that = (StopPattern) other; - return Arrays.equals(this.stops, that.stops) && - Arrays.equals(this.pickups, that.pickups) && - Arrays.equals(this.dropoffs, that.dropoffs); + return Arrays.equals(this.stops, that.stops) && + Arrays.equals(this.pickups, that.pickups) && + Arrays.equals(this.dropoffs, that.dropoffs) && + ((flexFields == null && that.flexFields == null) || + (flexFields != null && flexFields.equals(((StopPattern) other).flexFields))); } else { return false; } @@ -68,6 +77,7 @@ public int hashCode() { hash += Arrays.hashCode(this.pickups); hash *= 31; hash += Arrays.hashCode(this.dropoffs); + hash *= 31; return hash; } @@ -80,17 +90,22 @@ public String toString() { return sb.toString(); } - private StopPattern (int size) { - this.size = size; - stops = new Stop[size]; - pickups = new int[size]; - dropoffs = new int[size]; - } - - /** Assumes that stopTimes are already sorted by time. */ - public StopPattern (List stopTimes) { - this (stopTimes.size()); - if (size == 0) return; + /** + * Default constructor + * @param stopTimes List of StopTimes; assumes that stopTimes are already sorted by time. + * @param deduplicator Deduplicator. If null, do not deduplicate arrays. + */ + public StopPattern (List stopTimes, Deduplicator deduplicator) { + this.size = stopTimes.size(); + if (size == 0) { + this.stops = new Stop[size]; + this.pickups = new int[0]; + this.dropoffs = new int[0]; + return; + } + stops = new Stop[size]; + int[] pickups = new int[size]; + int[] dropoffs = new int[size]; for (int i = 0; i < size; ++i) { StopTime stopTime = stopTimes.get(i); stops[i] = stopTime.getStop(); @@ -109,6 +124,22 @@ public StopPattern (List stopTimes) { */ dropoffs[0] = 0; pickups[size - 1] = 0; + + if (deduplicator != null) { + this.pickups = deduplicator.deduplicateIntArray(pickups); + this.dropoffs = deduplicator.deduplicateIntArray(dropoffs); + } else { + this.pickups = pickups; + this.dropoffs = dropoffs; + } + } + + /** + * Create StopPattern without deduplicating arrays + * @param stopTimes List of StopTimes; assumes that stopTimes are already sorted by time. + */ + public StopPattern (List stopTimes) { + this(stopTimes, null); } /** @@ -120,8 +151,6 @@ public boolean containsStop (String stopId) { return false; } - private static final Logger LOG = LoggerFactory.getLogger(StopPattern.class); - /** * In most cases we want to use identity equality for StopPatterns. There is a single StopPattern instance for each * semantic StopPattern, and we don't want to calculate complicated hashes or equality values during normal @@ -142,7 +171,26 @@ public HashCode semanticHash(HashFunction hashFunction) { hasher.putInt(pickups[hop]); hasher.putInt(dropoffs[hop + 1]); } + if (flexFields != null) { + for (int hop = 0; hop < size - 1; hop++) { + hasher.putInt(flexFields.continuousPickup[hop]); + hasher.putInt(flexFields.continuousDropOff[hop]); + hasher.putDouble(flexFields.serviceAreaRadius[hop]); + hasher.putInt(flexFields.serviceAreas[hop].hashCode()); + } + } return hasher.hash(); } + public StopPatternFlexFields getFlexFields() { + return flexFields; + } + + public void setFlexFields(StopPatternFlexFields flexFields) { + this.flexFields = flexFields; + } + + public boolean hasFlexFields() { + return flexFields != null; + } } diff --git a/src/main/java/org/opentripplanner/model/StopPatternFlexFields.java b/src/main/java/org/opentripplanner/model/StopPatternFlexFields.java new file mode 100644 index 00000000000..9ae70feb936 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/StopPatternFlexFields.java @@ -0,0 +1,100 @@ +package org.opentripplanner.model; + +import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.routing.trippattern.Deduplicator; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Class to keep track of GTFS-Flex related StopPattern parameters + */ +public class StopPatternFlexFields implements Serializable { + + private static final long serialVersionUID = 1L; + + public final int size; + + public final int[] continuousPickup; + public final int[] continuousDropOff; + public final double[] serviceAreaRadius; + public final Geometry[] serviceAreas; // likely at most one distinct object will be in this array + + /** + * Default constructor. + * @param stopTimes stopTimes for this pattern + * @param areaGeometryByArea Map of GTFS-Flex areas, keyed by area ID + * @param deduplicator Deduplicator + */ + public StopPatternFlexFields(List stopTimes, Map areaGeometryByArea, Deduplicator deduplicator) { + size = stopTimes.size(); + if (size == 0) { + continuousPickup = new int[size]; + continuousDropOff = new int[size]; + serviceAreaRadius = new double[size]; + serviceAreas = new Geometry[size]; + return; + } + int[] continuousPickup = new int[size]; + int[] continuousDropOff = new int[size]; + double[] serviceAreaRadius = new double[size]; + serviceAreas = new Geometry[size]; + double lastServiceAreaRadius = 0; + Geometry lastServiceArea = null; + for (int i = 0; i < size; ++i) { + StopTime stopTime = stopTimes.get(i); + + // continuous stops can be empty, which means 1 (no continuous stopping behavior) + continuousPickup[i] = stopTime.getContinuousPickup() == StopTime.MISSING_VALUE ? 1 : stopTime.getContinuousPickup(); + continuousDropOff[i] = stopTime.getContinuousDropOff() == StopTime.MISSING_VALUE ? 1 : stopTime.getContinuousDropOff(); + + + if (stopTime.getStartServiceAreaRadius() != StopTime.MISSING_VALUE) { + lastServiceAreaRadius = stopTime.getStartServiceAreaRadius(); + } + serviceAreaRadius[i] = lastServiceAreaRadius; + if (stopTime.getEndServiceAreaRadius() != StopTime.MISSING_VALUE) { + lastServiceAreaRadius = 0; + } + + if (areaGeometryByArea != null) { + if (stopTime.getStartServiceArea() != null) { + lastServiceArea = areaGeometryByArea.get(stopTime.getStartServiceArea().getAreaId()); + } + serviceAreas[i] = lastServiceArea; + if (stopTime.getEndServiceArea() != null) { + lastServiceArea = null; + } + } + } + this.continuousPickup = deduplicator.deduplicateIntArray(continuousPickup); + this.continuousDropOff = deduplicator.deduplicateIntArray(continuousDropOff); + this.serviceAreaRadius = deduplicator.deduplicateDoubleArray(serviceAreaRadius); + + } + + @Override + public int hashCode() { + int hash = size; + hash += Arrays.hashCode(this.continuousPickup); + hash *= 31; + hash += Arrays.hashCode(this.continuousDropOff); + hash *= 31; + hash += Arrays.hashCode(this.serviceAreas); + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StopPatternFlexFields that = (StopPatternFlexFields) o; + if (size != that.size) return false; + if (!Arrays.equals(continuousPickup, that.continuousPickup)) return false; + if (!Arrays.equals(continuousDropOff, that.continuousDropOff)) return false; + if (!Arrays.equals(serviceAreaRadius, that.serviceAreaRadius)) return false; + return Arrays.equals(serviceAreas, that.serviceAreas); + } +} diff --git a/src/main/java/org/opentripplanner/model/StopTime.java b/src/main/java/org/opentripplanner/model/StopTime.java index 253a998ee1c..48fac060ce0 100644 --- a/src/main/java/org/opentripplanner/model/StopTime.java +++ b/src/main/java/org/opentripplanner/model/StopTime.java @@ -1,6 +1,7 @@ /* This file is based on code copied from project OneBusAway, see the LICENSE file for further information. */ package org.opentripplanner.model; +import org.onebusaway.gtfs.model.Area; import org.opentripplanner.util.TimeToStringConverter; import java.io.Serializable; @@ -34,6 +35,18 @@ public final class StopTime implements Serializable, Comparable { private double shapeDistTraveled = MISSING_VALUE; + private int continuousPickup = MISSING_VALUE; + + private int continuousDropOff = MISSING_VALUE; + + private Area startServiceArea; + + private Area endServiceArea; + + private double startServiceAreaRadius = MISSING_VALUE; + + private double endServiceAreaRadius = MISSING_VALUE; + /** This is a Conveyal extension to the GTFS spec to support Seattle on/off peak fares. */ private String farePeriodId; @@ -53,6 +66,12 @@ public StopTime(StopTime st) { this.stopSequence = st.stopSequence; this.timepoint = st.timepoint; this.trip = st.trip; + this.continuousPickup = st.continuousPickup; + this.continuousDropOff = st.continuousDropOff; + this.startServiceArea = st.startServiceArea; + this.endServiceArea = st.endServiceArea; + this.startServiceAreaRadius = st.startServiceAreaRadius; + this.endServiceAreaRadius = st.endServiceAreaRadius; } public Trip getTrip() { @@ -192,6 +211,54 @@ public void setFarePeriodId(String farePeriodId) { this.farePeriodId = farePeriodId; } + public int getContinuousPickup() { + return continuousPickup; + } + + public void setContinuousPickup(int continuousPickup) { + this.continuousPickup = continuousPickup; + } + + public int getContinuousDropOff() { + return continuousDropOff; + } + + public void setContinuousDropOff(int continuousDropOff) { + this.continuousDropOff = continuousDropOff; + } + + public Area getStartServiceArea() { + return startServiceArea; + } + + public void setStartServiceArea(Area startServiceArea) { + this.startServiceArea = startServiceArea; + } + + public Area getEndServiceArea() { + return endServiceArea; + } + + public void setEndServiceArea(Area endServiceArea) { + this.endServiceArea = endServiceArea; + } + + public double getStartServiceAreaRadius() { + return startServiceAreaRadius; + } + + public void setStartServiceAreaRadius(double startServiceAreaRadius) { + this.startServiceAreaRadius = startServiceAreaRadius; + } + + public double getEndServiceAreaRadius() { + return endServiceAreaRadius; + } + + public void setEndServiceAreaRadius(double endServiceAreaRadius) { + this.endServiceAreaRadius = endServiceAreaRadius; + } + public int compareTo(StopTime o) { return this.getStopSequence() - o.getStopSequence(); } diff --git a/src/main/java/org/opentripplanner/model/Trip.java b/src/main/java/org/opentripplanner/model/Trip.java index 8c055814430..fcead4194a9 100644 --- a/src/main/java/org/opentripplanner/model/Trip.java +++ b/src/main/java/org/opentripplanner/model/Trip.java @@ -35,6 +35,20 @@ public final class Trip extends IdentityBean { /** Custom extension for KCM to specify a fare per-trip */ private String fareId; + private String drtMaxTravelTime; + + private String drtAvgTravelTime; + + private double drtAdvanceBookMin; + + private String drtPickupMessage; + + private String drtDropOffMessage; + + private String continuousPickupMessage; + + private String continuousDropOffMessage; + public Trip() { } @@ -52,6 +66,13 @@ public Trip(Trip obj) { this.tripBikesAllowed = obj.tripBikesAllowed; this.bikesAllowed = obj.bikesAllowed; this.fareId = obj.fareId; + this.drtMaxTravelTime = obj.drtMaxTravelTime; + this.drtAvgTravelTime = obj.drtAvgTravelTime; + this.drtAdvanceBookMin = obj.drtAdvanceBookMin; + this.drtPickupMessage = obj.drtPickupMessage; + this.drtDropOffMessage = obj.drtDropOffMessage; + this.continuousPickupMessage = obj.continuousPickupMessage; + this.continuousDropOffMessage = obj.continuousDropOffMessage; } public FeedScopedId getId() { @@ -170,4 +191,60 @@ public void setFareId(String fareId) { this.fareId = fareId; } + public String getDrtMaxTravelTime() { + return drtMaxTravelTime; + } + + public void setDrtMaxTravelTime(String drtMaxTravelTime) { + this.drtMaxTravelTime = drtMaxTravelTime; + } + + public String getDrtAvgTravelTime() { + return drtAvgTravelTime; + } + + public void setDrtAvgTravelTime(String drtAvgTravelTime) { + this.drtAvgTravelTime = drtAvgTravelTime; + } + + public double getDrtAdvanceBookMin() { + return drtAdvanceBookMin; + } + + public void setDrtAdvanceBookMin(double drtAdvanceBookMin) { + this.drtAdvanceBookMin = drtAdvanceBookMin; + } + + public String getDrtPickupMessage() { + return drtPickupMessage; + } + + public void setDrtPickupMessage(String drtPickupMessage) { + this.drtPickupMessage = drtPickupMessage; + } + + public String getDrtDropOffMessage() { + return drtDropOffMessage; + } + + public void setDrtDropOffMessage(String drtDropOffMessage) { + this.drtDropOffMessage = drtDropOffMessage; + } + + public String getContinuousPickupMessage() { + return continuousPickupMessage; + } + + public void setContinuousPickupMessage(String continuousPickupMessage) { + this.continuousPickupMessage = continuousPickupMessage; + } + + public String getContinuousDropOffMessage() { + return continuousDropOffMessage; + } + + public void setContinuousDropOffMessage(String continuousDropOffMessage) { + this.continuousDropOffMessage = continuousDropOffMessage; + } + } diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java index cebefe7eac0..fd239b54f7b 100644 --- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java +++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java @@ -1,6 +1,7 @@ package org.opentripplanner.model.impl; import org.opentripplanner.model.Agency; +import org.opentripplanner.model.FlexArea; import org.opentripplanner.model.FareAttribute; import org.opentripplanner.model.FareRule; import org.opentripplanner.model.FeedInfo; @@ -54,6 +55,8 @@ public class OtpTransitServiceBuilder { private final List trips = new ArrayList<>(); + private final List flexAreas = new ArrayList<>(); + public OtpTransitServiceBuilder() { } @@ -76,6 +79,7 @@ public OtpTransitServiceBuilder add(OtpTransitService other) { stopTimes.addAll(other.getAllStopTimes()); transfers.addAll(other.getAllTransfers()); trips.addAll(other.getAllTrips()); + flexAreas.addAll(other.getAllAreas()); return this; } @@ -135,13 +139,17 @@ public List getTrips() { return trips; } + public List getFlexAreas() { + return flexAreas; + } + public OtpTransitService build() { createNoneExistentIds(); return new OtpTransitServiceImpl(agencies, calendarDates, calendars, fareAttributes, fareRules, feedInfos, frequencies, pathways, routes, shapePoints, stops, stopTimes, transfers, - trips); + trips, flexAreas); } private void createNoneExistentIds() { diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceImpl.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceImpl.java index 4487d4450f7..e7446e6069c 100644 --- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceImpl.java +++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceImpl.java @@ -2,6 +2,7 @@ package org.opentripplanner.model.impl; import org.opentripplanner.model.Agency; +import org.opentripplanner.model.FlexArea; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.FareAttribute; import org.opentripplanner.model.FareRule; @@ -74,6 +75,8 @@ class OtpTransitServiceImpl implements OtpTransitService { private Collection trips; + private Collection flexAreas; + // Indexes private Map> tripAgencyIdsByServiceId = null; @@ -96,7 +99,7 @@ class OtpTransitServiceImpl implements OtpTransitService { List fareRules, List feedInfos, List frequencies, List pathways, List routes, List shapePoints, List stops, List stopTimes, List transfers, - List trips) { + List trips, List flexAreas) { this.agencies = nullSafeUnmodifiableList(agencies); this.calendarDates = nullSafeUnmodifiableList(calendarDates); this.calendars = nullSafeUnmodifiableList(calendars); @@ -111,6 +114,7 @@ class OtpTransitServiceImpl implements OtpTransitService { this.stopTimes = nullSafeUnmodifiableList(stopTimes); this.transfers = nullSafeUnmodifiableList(transfers); this.trips = nullSafeUnmodifiableList(trips); + this.flexAreas = nullSafeUnmodifiableList(flexAreas); } @Override @@ -284,6 +288,10 @@ public ServiceCalendar getCalendarForServiceId(FeedScopedId serviceId) { throw new MultipleCalendarsForServiceIdException(serviceId); } + @Override + public Collection getAllAreas() { + return flexAreas; + } /* Private Methods */ diff --git a/src/main/java/org/opentripplanner/model/json_serialization/BogusGeometryFactory.java b/src/main/java/org/opentripplanner/model/json_serialization/BogusGeometryFactory.java index 57a1860a735..933abe646e2 100644 --- a/src/main/java/org/opentripplanner/model/json_serialization/BogusGeometryFactory.java +++ b/src/main/java/org/opentripplanner/model/json_serialization/BogusGeometryFactory.java @@ -1,6 +1,6 @@ package org.opentripplanner.model.json_serialization; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; /** * Enunciate requires that we actually be able to construct objects, but diff --git a/src/main/java/org/opentripplanner/model/json_serialization/CoordinateSerializer.java b/src/main/java/org/opentripplanner/model/json_serialization/CoordinateSerializer.java index b893e2ffc78..84cfb6d104e 100644 --- a/src/main/java/org/opentripplanner/model/json_serialization/CoordinateSerializer.java +++ b/src/main/java/org/opentripplanner/model/json_serialization/CoordinateSerializer.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; public class CoordinateSerializer extends JsonSerializer { diff --git a/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONDeserializer.java b/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONDeserializer.java index 78eb233f4cb..6fd8b9cf037 100644 --- a/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONDeserializer.java +++ b/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONDeserializer.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.util.PolylineEncoder; import org.opentripplanner.util.model.EncodedPolylineBean; diff --git a/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializer.java b/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializer.java index 96823ff7f37..b8c4c9b274b 100644 --- a/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializer.java +++ b/src/main/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializer.java @@ -12,8 +12,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; public class EncodedPolylineJSONSerializer extends JsonSerializer { diff --git a/src/main/java/org/opentripplanner/model/json_serialization/SerializerUtils.java b/src/main/java/org/opentripplanner/model/json_serialization/SerializerUtils.java index 17f174856cf..60b4b8d0e6b 100644 --- a/src/main/java/org/opentripplanner/model/json_serialization/SerializerUtils.java +++ b/src/main/java/org/opentripplanner/model/json_serialization/SerializerUtils.java @@ -1,6 +1,5 @@ package org.opentripplanner.model.json_serialization; -import com.conveyal.geojson.GeometrySerializer; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.AnnotationIntrospector; @@ -9,6 +8,7 @@ import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; +import org.opentripplanner.common.geometry.GeometrySerializer; public class SerializerUtils { diff --git a/src/main/java/org/opentripplanner/openstreetmap/services/RegionsSource.java b/src/main/java/org/opentripplanner/openstreetmap/services/RegionsSource.java index 95fc7284cb2..74716147ce5 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/services/RegionsSource.java +++ b/src/main/java/org/opentripplanner/openstreetmap/services/RegionsSource.java @@ -1,6 +1,6 @@ package org.opentripplanner.openstreetmap.services; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; /** * A RegionSource represents a set of rectangular regions. It's used by diff --git a/src/main/java/org/opentripplanner/profile/IsochroneGenerator.java b/src/main/java/org/opentripplanner/profile/IsochroneGenerator.java index 9b81b9dff62..84cf19f4812 100644 --- a/src/main/java/org/opentripplanner/profile/IsochroneGenerator.java +++ b/src/main/java/org/opentripplanner/profile/IsochroneGenerator.java @@ -1,6 +1,6 @@ package org.opentripplanner.profile; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import org.apache.commons.math3.util.FastMath; import org.opentripplanner.analyst.PointSet; import org.opentripplanner.analyst.core.IsochroneData; diff --git a/src/main/java/org/opentripplanner/profile/PropagatedTimesStore.java b/src/main/java/org/opentripplanner/profile/PropagatedTimesStore.java index 3cd7c081a62..422eeb49d54 100644 --- a/src/main/java/org/opentripplanner/profile/PropagatedTimesStore.java +++ b/src/main/java/org/opentripplanner/profile/PropagatedTimesStore.java @@ -1,6 +1,6 @@ package org.opentripplanner.profile; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import gnu.trove.list.TIntList; import gnu.trove.list.array.TIntArrayList; import org.apache.commons.math3.util.FastMath; diff --git a/src/main/java/org/opentripplanner/profile/RoundBasedProfileRouter.java b/src/main/java/org/opentripplanner/profile/RoundBasedProfileRouter.java index 9835979ab25..e9f7e288450 100644 --- a/src/main/java/org/opentripplanner/profile/RoundBasedProfileRouter.java +++ b/src/main/java/org/opentripplanner/profile/RoundBasedProfileRouter.java @@ -17,11 +17,11 @@ import com.google.common.base.Predicate; import com.google.common.collect.Collections2; -import org.mapdb.Fun.Tuple2; import org.opentripplanner.analyst.TimeSurface; import org.opentripplanner.analyst.TimeSurface.RangeSet; import org.opentripplanner.api.parameter.QualifiedModeSet; import org.opentripplanner.common.model.GenericLocation; +import org.opentripplanner.common.model.T2; import org.opentripplanner.profile.ProfileState.Type; import org.opentripplanner.routing.algorithm.AStar; import org.opentripplanner.routing.core.RoutingRequest; @@ -244,14 +244,14 @@ public boolean apply(ProfileState input) { // avoid concurrent modification Set touchedStopKeys = new HashSet(store.keys()); for (TransitStop tstop : touchedStopKeys) { - List> accessTimes = Lists.newArrayList(); + List> accessTimes = Lists.newArrayList(); // find transfers for the stop for (Edge e : tstop.getOutgoing()) { if (e instanceof SimpleTransfer) { SimpleTransfer t = (SimpleTransfer) e; int time = (int) (t.getDistance() / request.walkSpeed); - accessTimes.add(new Tuple2((TransitStop) e.getToVertex(), time)); + accessTimes.add(new T2((TransitStop) e.getToVertex(), time)); } } @@ -268,20 +268,20 @@ public boolean apply(ProfileState input) { HashSet patternsAtSource = new HashSet(graph.index.patternsForStop.get(tstop.getStop())); for (ProfileState ps : statesAtStop) { - for (Tuple2 atime : accessTimes) { - ProfileState ps2 = ps.propagate(atime.b); + for (T2 atime : accessTimes) { + ProfileState ps2 = ps.propagate(atime.second); ps2.accessType = Type.TRANSFER; - ps2.stop = atime.a; + ps2.stop = atime.first; // note that we do not reset pattern, as we still don't want to transfer from a pattern to itself. // (TODO: is this true? loop routes?) - for (TripPattern patt : graph.index.patternsForStop.get(atime.a.getStop())) { + for (TripPattern patt : graph.index.patternsForStop.get(atime.first.getStop())) { // don't transfer to patterns that we can board at this stop. if (patternsAtSource.contains(patt)) continue; - if (atime.b < minBoardTime.get(patt)) { - minBoardTime.put(patt, atime.b); + if (atime.second < minBoardTime.get(patt)) { + minBoardTime.put(patt, atime.second); optimalBoardState.put(patt, ps2); } } diff --git a/src/main/java/org/opentripplanner/profile/StreetSegment.java b/src/main/java/org/opentripplanner/profile/StreetSegment.java index 5181a3cbd66..c1b1d4476e9 100644 --- a/src/main/java/org/opentripplanner/profile/StreetSegment.java +++ b/src/main/java/org/opentripplanner/profile/StreetSegment.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.google.common.collect.Lists; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.api.model.Itinerary; import org.opentripplanner.api.model.Leg; import org.opentripplanner.api.model.WalkStep; diff --git a/src/main/java/org/opentripplanner/profile/TNPropagatedTimesStore.java b/src/main/java/org/opentripplanner/profile/TNPropagatedTimesStore.java index 527e467024a..64a291bc943 100644 --- a/src/main/java/org/opentripplanner/profile/TNPropagatedTimesStore.java +++ b/src/main/java/org/opentripplanner/profile/TNPropagatedTimesStore.java @@ -1,6 +1,6 @@ package org.opentripplanner.profile; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import gnu.trove.list.TIntList; import gnu.trove.list.array.TIntArrayList; import org.apache.commons.math3.util.FastMath; diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/Alert.java b/src/main/java/org/opentripplanner/routing/alertpatch/Alert.java index 7cfe1a045a9..bdb31ec7ec4 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/Alert.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/Alert.java @@ -7,28 +7,19 @@ import java.util.Date; import java.util.HashSet; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlType; - -@XmlType public class Alert implements Serializable { private static final long serialVersionUID = 8305126586053909836L; - @XmlElement public I18NString alertHeaderText; - @XmlElement public I18NString alertDescriptionText; - @XmlElement public I18NString alertUrl; //null means unknown - @XmlElement public Date effectiveStartDate; //null means unknown - @XmlElement public Date effectiveEndDate; public static HashSet newSimpleAlertSet(String text) { diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/AlertPatch.java b/src/main/java/org/opentripplanner/routing/alertpatch/AlertPatch.java index d6c0480dd89..a6429cbadcc 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/AlertPatch.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/AlertPatch.java @@ -5,10 +5,6 @@ import java.io.Serializable; import java.util.*; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - import org.opentripplanner.model.Agency; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.Route; @@ -31,7 +27,6 @@ * @author novalis * */ -@XmlRootElement(name = "AlertPatch") public class AlertPatch implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(AlertPatch.class); @@ -66,7 +61,6 @@ public class AlertPatch implements Serializable { */ private int directionId = -1; - @XmlElement public Alert getAlert() { return alert; } @@ -82,7 +76,6 @@ public boolean displayDuring(State state) { return false; } - @XmlElement public String getId() { return id; } @@ -232,17 +225,14 @@ public String getAgency() { return agency; } - @XmlJavaTypeAdapter(AgencyAndIdAdapter.class) public FeedScopedId getRoute() { return route; } - @XmlJavaTypeAdapter(AgencyAndIdAdapter.class) public FeedScopedId getTrip() { return trip; } - @XmlJavaTypeAdapter(AgencyAndIdAdapter.class) public FeedScopedId getStop() { return stop; } @@ -270,12 +260,10 @@ public void setDirectionId(int direction) { this.directionId = direction; } - @XmlElement public String getDirection() { return direction; } - @XmlElement public int getDirectionId() { return directionId; } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/strategies/InterleavedBidirectionalHeuristic.java b/src/main/java/org/opentripplanner/routing/algorithm/strategies/InterleavedBidirectionalHeuristic.java index 3ef45b1373c..ea9cc89d5f2 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/strategies/InterleavedBidirectionalHeuristic.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/strategies/InterleavedBidirectionalHeuristic.java @@ -345,7 +345,7 @@ private double getPreTransitRemainingWeightEstimate(Vertex v, double preTransitS target.getLat(), target.getLon() ) / remainingDistanceSpeed - ) + ) : 0; } diff --git a/src/main/java/org/opentripplanner/routing/bike_rental/BikeRentalStation.java b/src/main/java/org/opentripplanner/routing/bike_rental/BikeRentalStation.java index 166678e5b46..2873f6dd60d 100644 --- a/src/main/java/org/opentripplanner/routing/bike_rental/BikeRentalStation.java +++ b/src/main/java/org/opentripplanner/routing/bike_rental/BikeRentalStation.java @@ -5,9 +5,6 @@ import java.util.Locale; import java.util.Set; -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlTransient; - import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.opentripplanner.routing.vehicle_rental.RentalStation; import org.opentripplanner.util.I18NString; @@ -16,24 +13,34 @@ public class BikeRentalStation extends RentalStation implements Serializable, Cloneable { private static final long serialVersionUID = 8311460609708089384L; - @XmlAttribute + @JsonSerialize + public String id; + //Serialized in TranslatedBikeRentalStation + @JsonIgnore + public I18NString name; + @JsonSerialize + public double x, y; //longitude, latitude @JsonSerialize public int bikesAvailable = Integer.MAX_VALUE; - @XmlAttribute @JsonSerialize public int spacesAvailable = Integer.MAX_VALUE; - @XmlAttribute + @JsonSerialize + public boolean allowDropoff = true; @JsonSerialize public boolean isFloatingBike = false; - @XmlAttribute @JsonSerialize public boolean isCarStation = false; - + + /** + * List of compatible network names. Null (default) to be compatible with all. + */ + @JsonSerialize + public Set networks = null; + /** * Whether this station is static (usually coming from OSM data) or a real-time source. If no real-time data, users should take * bikesAvailable/spacesAvailable with a pinch of salt, as they are always the total capacity divided by two. Only the total is meaningful. */ - @XmlAttribute @JsonSerialize public boolean realTimeData = true; @@ -50,7 +57,6 @@ public class BikeRentalStation extends RentalStation implements Serializable, Cl * */ @JsonIgnore - @XmlTransient public Locale locale = ResourceBundleSingleton.INSTANCE.getLocale(null); /** @@ -64,13 +70,13 @@ public boolean equals(Object o) { BikeRentalStation other = (BikeRentalStation) o; return other.id.equals(id); } - + public int hashCode() { return id.hashCode() + 1; } - + public String toString () { - return String.format(Locale.US, "Bike rental station %s at %.6f, %.6f", name, y, x); + return String.format(Locale.US, "Bike rental station %s at %.6f, %.6f", name, y, x); } @Override @@ -85,7 +91,6 @@ public BikeRentalStation clone() { /** * Gets translated name of bike rental station based on locale */ - @XmlAttribute @JsonSerialize public String getName() { return name.toString(locale); diff --git a/src/main/java/org/opentripplanner/routing/car_rental/CarRentalRegion.java b/src/main/java/org/opentripplanner/routing/car_rental/CarRentalRegion.java index 51d4a0273de..aad093e0167 100644 --- a/src/main/java/org/opentripplanner/routing/car_rental/CarRentalRegion.java +++ b/src/main/java/org/opentripplanner/routing/car_rental/CarRentalRegion.java @@ -14,7 +14,7 @@ the License, or (at your option) any later version. package org.opentripplanner.routing.car_rental; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; import javax.xml.bind.annotation.XmlAttribute; import java.io.Serializable; diff --git a/src/main/java/org/opentripplanner/routing/core/Fare.java b/src/main/java/org/opentripplanner/routing/core/Fare.java index 7baaa32900c..b36ca0ebcda 100644 --- a/src/main/java/org/opentripplanner/routing/core/Fare.java +++ b/src/main/java/org/opentripplanner/routing/core/Fare.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -24,12 +25,15 @@ public static enum FareType implements Serializable { /** * A mapping from {@link FareType} to a list of {@link FareComponent}. + * The FareComponents are stored in an array instead of a list because JAXB doesn't know how to deal with + * interfaces when serializing a trip planning response, and List is an interface. + * See https://stackoverflow.com/a/1119241/778449 */ - public HashMap> details; + public HashMap details; public Fare() { - fare = new HashMap(); - details = new HashMap>(); + fare = new HashMap<>(); + details = new HashMap<>(); } public Fare(Fare aFare) { @@ -51,7 +55,7 @@ public void addFare(FareType fareType, Money money) { } public void addFareDetails(FareType fareType, List newDetails) { - details.put(fareType, newDetails); + details.put(fareType, newDetails.toArray(new FareComponent[newDetails.size()])); } public Money getFare(FareType type) { @@ -59,7 +63,7 @@ public Money getFare(FareType type) { } public List getDetails(FareType type) { - return details.get(type); + return Arrays.asList(details.get(type)); } public void addCost(int surcharge) { diff --git a/src/main/java/org/opentripplanner/routing/core/RoutingContext.java b/src/main/java/org/opentripplanner/routing/core/RoutingContext.java index 4ed53c7a5ac..085b1c8bd36 100644 --- a/src/main/java/org/opentripplanner/routing/core/RoutingContext.java +++ b/src/main/java/org/opentripplanner/routing/core/RoutingContext.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.core; import com.google.common.collect.Iterables; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.model.Agency; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.Stop; @@ -26,7 +26,6 @@ import org.opentripplanner.routing.services.OnBoardDepartService; import org.opentripplanner.routing.vertextype.TemporaryVertex; import org.opentripplanner.routing.vertextype.TransitStop; -import org.opentripplanner.traffic.StreetSpeedSnapshot; import org.opentripplanner.updater.stoptime.TimetableSnapshotSource; import org.opentripplanner.util.NonLocalizedString; import org.slf4j.Logger; @@ -34,11 +33,15 @@ import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TimeZone; /** * A RoutingContext holds information needed to carry out a search for a particular TraverseOptions, on a specific graph. @@ -66,7 +69,7 @@ public class RoutingContext implements Cloneable { // target means "where this search will terminate" not "the end of the trip from the user's perspective" public final Vertex target; - + // The back edge associated with the origin - i.e. continuing a previous search. // NOTE: not final so that it can be modified post-construction for testing. // TODO(flamholz): figure out a better way. @@ -84,9 +87,6 @@ public class RoutingContext implements Cloneable { /** The timetableSnapshot is a {@link TimetableSnapshot} for looking up real-time updates. */ public final TimetableSnapshot timetableSnapshot; - /** A snapshot of street speeds for looking up real-time or historical traffic data */ - public final StreetSpeedSnapshot streetSpeedSnapshot; - /** * Cache lists of which transit services run on which midnight-to-midnight periods. This ties a TraverseOptions to a particular start time for the * duration of a search so the same options cannot be used for multiple searches concurrently. To do so this cache would need to be moved into @@ -113,20 +113,36 @@ public class RoutingContext implements Cloneable { /** Indicates that a maximum slope constraint was specified but was removed during routing to produce a result. */ public boolean slopeRestrictionRemoved = false; + /** + * Temporary vertices created during the request. This is needed for GTFS-Flex support. The other temporary vertices which are created have + * known locations (the endpoints of the search), but GTFS-Flex routing may require temporary vertices to be created at other places in the + * graph. Temporary vertices are request-specific need to be disposed of at the end-of-life of a request. + */ + public Collection temporaryVertices = new ArrayList<>(); + /* CONSTRUCTORS */ /** * Constructor that automatically computes origin/target from RoutingRequest. */ public RoutingContext(RoutingRequest routingRequest, Graph graph) { - this(routingRequest, graph, null, null, true); + this(routingRequest, graph, null, null, true, null); + } + + /** + * Constructor that automatically computes origin/target from RoutingRequest, and sets the + * context's temporary vertices. This is needed for intermediate places, as a consequence of + * the check that temporary vertices are request-specific. + */ + public RoutingContext(RoutingRequest routingRequest, Graph graph, Collection temporaryVertices) { + this(routingRequest, graph, null, null, true, temporaryVertices); } /** * Constructor that takes to/from vertices as input. */ public RoutingContext(RoutingRequest routingRequest, Graph graph, Vertex from, Vertex to) { - this(routingRequest, graph, from, to, false); + this(routingRequest, graph, from, to, false, null); } /** @@ -163,16 +179,16 @@ private Set overlappingStreetEdges(Vertex u, Vertex v) { * Creates a PartialStreetEdge along the input StreetEdge iff its direction makes this possible. */ private void makePartialEdgeAlong(StreetEdge streetEdge, TemporaryStreetLocation from, - TemporaryStreetLocation to) { + TemporaryStreetLocation to) { LineString parent = streetEdge.getGeometry(); LineString head = GeometryUtils.getInteriorSegment(parent, - streetEdge.getFromVertex().getCoordinate(), from.getCoordinate()); + streetEdge.getFromVertex().getCoordinate(), from.getCoordinate()); LineString tail = GeometryUtils.getInteriorSegment(parent, - to.getCoordinate(), streetEdge.getToVertex().getCoordinate()); + to.getCoordinate(), streetEdge.getToVertex().getCoordinate()); if (parent.getLength() > head.getLength() + tail.getLength()) { LineString partial = GeometryUtils.getInteriorSegment(parent, - from.getCoordinate(), to.getCoordinate()); + from.getCoordinate(), to.getCoordinate()); double lengthRatio = partial.getLength() / parent.getLength(); double length = streetEdge.getDistance() * lengthRatio; @@ -185,13 +201,14 @@ private void makePartialEdgeAlong(StreetEdge streetEdge, TemporaryStreetLocation /** * Flexible constructor which may compute to/from vertices. - * + * * TODO(flamholz): delete this flexible constructor and move the logic to constructors above appropriately. - * + * * @param findPlaces if true, compute origin and target from RoutingRequest using spatial indices. + * @param temporaryVerticesParam if not null, use this collection to keep track of temporary vertices. */ private RoutingContext(RoutingRequest routingRequest, Graph graph, Vertex from, Vertex to, - boolean findPlaces) { + boolean findPlaces, Collection temporaryVerticesParam) { if (graph == null) { throw new GraphNotFoundException(); } @@ -223,12 +240,6 @@ private RoutingContext(RoutingRequest routingRequest, Graph graph, Vertex from, calendarService = null; } - // do the same for traffic - if (graph.streetSpeedSource != null) - this.streetSpeedSnapshot = graph.streetSpeedSource.getSnapshot(); - else - this.streetSpeedSnapshot = null; - Edge fromBackEdge = null; Edge toBackEdge = null; if (findPlaces) { @@ -279,17 +290,25 @@ private RoutingContext(RoutingRequest routingRequest, Graph graph, Vertex from, // TODO(flamholz): seems like this might be the wrong place for this code? Can't find a better one. // if (fromVertex instanceof TemporaryStreetLocation && - toVertex instanceof TemporaryStreetLocation) { + toVertex instanceof TemporaryStreetLocation) { TemporaryStreetLocation fromStreetVertex = (TemporaryStreetLocation) fromVertex; TemporaryStreetLocation toStreetVertex = (TemporaryStreetLocation) toVertex; Set overlap = overlappingStreetEdges(fromStreetVertex, - toStreetVertex); + toStreetVertex); for (StreetEdge pse : overlap) { makePartialEdgeAlong(pse, fromStreetVertex, toStreetVertex); } } + // Add temporary subgraphs to the routing context. If `fromVertex` or `toVertex` are not + // tempoorary vertices, their subgraphs are empty, so this has no effect. + if (temporaryVerticesParam != null) { + temporaryVertices = temporaryVerticesParam; + } + temporaryVertices.addAll(TemporaryVertex.findSubgraph(fromVertex)); + temporaryVertices.addAll(TemporaryVertex.findSubgraph(toVertex)); + if (opt.startingTransitStopId != null) { Stop stop = graph.index.stopForId.get(opt.startingTransitStopId); TransitStop tstop = graph.index.stopVertexForStop.get(stop); @@ -355,20 +374,34 @@ private void setServiceDays() { final ServiceDate serviceDate = new ServiceDate(c); this.serviceDays = new ArrayList(3); if (calendarService == null && graph.getCalendarService() != null - && (opt.modes == null || opt.modes.contains(TraverseMode.TRANSIT))) { + && (opt.modes == null || opt.modes.contains(TraverseMode.TRANSIT))) { LOG.warn("RoutingContext has no CalendarService. Transit will never be boarded."); return; } - for (String feedId : graph.getFeedIds()) { - for (Agency agency : graph.getAgencies(feedId)) { - addIfNotExists(this.serviceDays, new ServiceDay(graph, serviceDate.previous(), - calendarService, agency.getId())); - addIfNotExists(this.serviceDays, new ServiceDay(graph, serviceDate, calendarService, agency.getId())); - addIfNotExists(this.serviceDays, new ServiceDay(graph, serviceDate.next(), - calendarService, agency.getId())); + // In general, there should be a set of service days (yesterday, today, tomorrow) for every + // timezone in the graph. The parameter `serviceDayLookout` allows more service days to be + // added to search for future service (or past, if arriveBy=true). The furthest back trips + // can begin is 1 service day (e.g. a trip which started yesterday is usable today.) This + // does not address the case where a trip started multiple days ago (e.g. a multi-day ferry + // trip will not be board-able after day 2). + for (TimeZone timeZone : graph.getAllTimeZones()) { + // Add today + addIfNotExists(this.serviceDays, new ServiceDay(graph, serviceDate, calendarService, timeZone)); + // Add one day previous (previous in the direction of the transit search, so yesterday if + // arriveBy=false and tomorrow if arriveBy=true + addIfNotExists(this.serviceDays, new ServiceDay(graph, + opt.arriveBy ? serviceDate.next() : serviceDate.previous(), + calendarService, timeZone)); + // Add one or more days in the "forward" direction + ServiceDate sd = serviceDate; + int lookout = Math.max(1, opt.serviceDayLookout); + for (int i = 0; i < lookout; i++) { + sd = opt.arriveBy ? sd.previous() : sd.next(); + addIfNotExists(this.serviceDays, new ServiceDay(graph, sd, calendarService, timeZone)); } } + serviceDays.sort(Comparator.comparing(ServiceDay::getServiceDate)); } private static void addIfNotExists(ArrayList list, T item) { @@ -403,7 +436,7 @@ public boolean isWheelchairAccessible(Vertex v) { * for garbage collection. */ public void destroy() { - TemporaryVertex.dispose(fromVertex); - TemporaryVertex.dispose(toVertex); + TemporaryVertex.disposeAll(temporaryVertices); + temporaryVertices.clear(); } } \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/routing/core/RoutingRequest.java b/src/main/java/org/opentripplanner/routing/core/RoutingRequest.java index 38d0241201c..0ae93bfa091 100644 --- a/src/main/java/org/opentripplanner/routing/core/RoutingRequest.java +++ b/src/main/java/org/opentripplanner/routing/core/RoutingRequest.java @@ -157,14 +157,14 @@ public class RoutingRequest implements Cloneable, Serializable { * An extra penalty added on transfers (i.e. all boardings except the first one). * Not to be confused with bikeBoardCost and walkBoardCost, which are the cost of boarding a * vehicle with and without a bicycle. The boardCosts are used to model the 'usual' perceived - * cost of using a transit vehicle, and the transferPenalty is used when a user requests even - * less transfers. In the latter case, we don't actually optimize for fewest transfers, as this - * can lead to absurd results. Consider a trip in New York from Grand Army - * Plaza (the one in Brooklyn) to Kalustyan's at noon. The true lowest transfers route is to - * wait until midnight, when the 4 train runs local the whole way. The actual fastest route is + * cost of using a transit vehicle, and the transferPenalty is used when a user requests even + * less transfers. In the latter case, we don't actually optimize for fewest transfers, as this + * can lead to absurd results. Consider a trip in New York from Grand Army + * Plaza (the one in Brooklyn) to Kalustyan's at noon. The true lowest transfers route is to + * wait until midnight, when the 4 train runs local the whole way. The actual fastest route is * the 2/3 to the 4/5 at Nevins to the 6 at Union Square, which takes half an hour. - * Even someone optimizing for fewest transfers doesn't want to wait until midnight. Maybe they - * would be willing to walk to 7th Ave and take the Q to Union Square, then transfer to the 6. + * Even someone optimizing for fewest transfers doesn't want to wait until midnight. Maybe they + * would be willing to walk to 7th Ave and take the Q to Union Square, then transfer to the 6. * If this takes less than optimize_transfer_penalty seconds, then that's what we'll return. */ public int transferPenalty = 0; @@ -177,7 +177,7 @@ public class RoutingRequest implements Cloneable, Serializable { /** Used instead of walk reluctance for stairs */ public double stairsReluctance = 2.0; - + /** Multiplicative factor on expected turning time. */ public double turnReluctance = 1.0; @@ -307,14 +307,14 @@ public class RoutingRequest implements Cloneable, Serializable { public HashMap bannedTrips = new HashMap(); /** Do not use certain stops. See for more information the bannedStops property in the RoutingResource class. */ - public StopMatcher bannedStops = StopMatcher.emptyMatcher(); - + public StopMatcher bannedStops = StopMatcher.emptyMatcher(); + /** Do not use certain stops. See for more information the bannedStopsHard property in the RoutingResource class. */ public StopMatcher bannedStopsHard = StopMatcher.emptyMatcher(); - + /** Set of preferred routes by user. */ public RouteMatcher preferredRoutes = RouteMatcher.emptyMatcher(); - + /** Set of preferred agencies by user. */ public HashSet preferredAgencies = new HashSet(); @@ -326,7 +326,7 @@ public class RoutingRequest implements Cloneable, Serializable { /** Set of unpreferred routes for given user. */ public RouteMatcher unpreferredRoutes = RouteMatcher.emptyMatcher(); - + /** Set of unpreferred agencies for given user. */ public HashSet unpreferredAgencies = new HashSet(); @@ -351,8 +351,8 @@ public class RoutingRequest implements Cloneable, Serializable { public int maxTransfers = 2; /** - * Extensions to the trip planner will require additional traversal options beyond the default - * set. We provide an extension point for adding arbitrary parameters with an + * Extensions to the trip planner will require additional traversal options beyond the default + * set. We provide an extension point for adding arbitrary parameters with an * extension-specific key. */ public Map extensions = new HashMap(); @@ -361,7 +361,7 @@ public class RoutingRequest implements Cloneable, Serializable { public int nonpreferredTransferPenalty = 180; /** - * For the bike triangle, how important time is. + * For the bike triangle, how important time is. * triangleTimeFactor+triangleSlopeFactor+triangleSafetyFactor == 1 */ public double triangleTimeFactor; @@ -428,7 +428,7 @@ public class RoutingRequest implements Cloneable, Serializable { */ // 2.9 m/s/s: 0 mph to 65 mph in 10 seconds public double carAccelerationSpeed = 2.9; - + /** * When true, realtime updates are ignored during this search. */ @@ -440,13 +440,134 @@ public class RoutingRequest implements Cloneable, Serializable { */ public boolean disableRemainingWeightHeuristic = false; + /** + * Extra penalty added for flag-stop boarding/alighting. This parameter only applies to + * GTFS-Flex routing, which must be explicitly turned on via the useFlexService parameter + * in router-config.json. + * + * In GTFS-Flex, a flag stop is a point at which a vehicle is boarded or alighted which is not + * a defined stop, e.g. the bus is flagged down in between stops. This parameter is an + * additional cost added when a board/alight occurs at a flag stop. Increasing this parameter + * increases the cost of using a flag stop relative to a regular scheduled stop. + */ + public int flexFlagStopExtraPenalty = 90; + + /** + * Extra penalty added for deviated-route boarding/alighting. This parameter only applies to + * GTFS-Flex routing, which must be explicitly turned on via the useFlexService parameter + * in router-config.json. + * + * In GTFS-Flex, deviated-route service is when a vehicle can deviate a certain distance + * (or within a certain area) in order to drop off or pick up a passenger. This parameter is an + * additional cost added when a board/alight occurs before/after a deviation. Increasing this + * parameter increases the cost of using deviated-route service relative to fixed-route. + */ + public int flexDeviatedRouteExtraPenalty = 180; + + /** + * Reluctance for call-n-ride. This parameter only applies to GTFS-Flex routing, which must be + * explicitly turned on via the useFlexService parameter in router-config.json. + * + * Call-and-ride service is when a vehicle picks up and drops off a passenger at their origin + * and destination, without regard to a fixed route. In the GTFS-Flex data standard, call-and- + * ride service is defined analogously to deviated-route service, but with more permissive + * parameters. Depending on the particular origin and destination and the size of the area in + * which a route can deviate, a single route could be used for both deviated-route and call- + * and-ride service. This parameter is multiplied with the time on board call-and-ride in order to + * increase the cost of call-and-ride's use relative to normal transit. + */ + public double flexCallAndRideReluctance = 2.0; + + /** + * Total time which can be spent on a call-n-ride leg. This parameter only applies to GTFS-Flex + * routing, which must be explicitly turned on via the useFlexService parameter in + * router-config.json. + * + * "Trip-banning" as a method of obtaining different itinerary results does not work for call- + * and-ride service: the same trip can be used in different ways, for example to drop off a + * passenger at different transfer points. Thus, rather than trip-banning, after each itinerary + * is found, flexMaxCallAndRideSeconds is reduced in order to obtain different itineraries. The + * new value of the parameter to calculated according to the following formula: + * min(duration - options.flexReduceCallAndRideSeconds, duration * flexReduceCallAndRideRatio) + */ + public int flexMaxCallAndRideSeconds = Integer.MAX_VALUE; + + /** + * Control the reduction of call-and-ride time. This parameter only applies to GTFS-Flex + * routing, which must be explicitly turned on via the useFlexService parameter in + * router-config.json. + * + * Seconds to reduce flexMaxCallAndRideSeconds after a complete call-n-ride itinerary. The + * rationale for this parameter is given in the docs for flexMaxCallAndRideSeconds. + */ + public int flexReduceCallAndRideSeconds = 15 * 60; + + /** + * Control the reduction of call-and-ride time. This parameter only applies to GTFS-Flex + * routing, which must be explicitly turned on via the useFlexService parameter in + * router-config.json. + * + * Percentage to reduce flexMaxCallAndRideSeconds after a complete call-n-ride itinerary. The + * rationale for this parameter is given in the docs for flexMaxCallAndRideSeconds. + */ + public double flexReduceCallAndRideRatio = 0.5; + + /** + * Control the size of flag-stop buffer returned in API response. This parameter only applies + * to GTFS-Flex routing, which must be explicitly turned on via the useFlexService parameter in + * router-config.json. + * + * This allows the UI to specify the length in meters of a segment around flag stops it wants + * to display, as an indication to the user that the vehicle may be flagged down anywhere on + * the segment. The backend will supply such a cropped geometry in its response + * (`Place.flagStopArea`). The segment will be up to flexFlagStopBufferSize meters ahead or + * behind the board/alight location. The actual length may be less if the board/alight location + * is near the beginning or end of a route. + */ + public double flexFlagStopBufferSize; + + /** + * Whether to use reservation-based services. This parameter only applies to GTFS-Flex + * routing, which must be explicitly turned on via the useFlexService parameter in + * router-config.json. + * + * In GTFS-Flex, some trips may be defined as "reservation services," which indicates that + * they require a reservation in order to be used. Such services will only be used if this + * parameter is true. + */ + public boolean flexUseReservationServices = true; + + /** + * Whether to use eligibility-based services. This parameter only applies to GTFS-Flex + * routing, which must be explicitly turned on via the useFlexService parameter in + * router-config.json. + * + * In GTFS-Flex, some trips may be defined as "eligibility services," which indicates that + * they require customers to meet a certain set of requirements in order to be used. Such + * services will only be used if this parameter is true. + */ + public boolean flexUseEligibilityServices = true; + + /** + * Whether to ignore DRT time limits. This parameter only applies to GTFS-Flex routing, which + * must be explicitly turned on via the useFlexService parameter in router-config.json. + * + * In GTFS-Flex, deviated-route and call-and-ride service can define a trip-level parameter + * `drt_advance_book_min`, which determines how far in advance the flexible segment must be + * scheduled. If `flexIgnoreDrtAdvanceBookMin = false`, OTP will only provide itineraries which + * are feasible based on that constraint. For example, if the current time is 1:00pm and a + * particular service must be scheduled one hour in advance, the earliest time the service + * is usable is 2:00pm. + */ + public boolean flexIgnoreDrtAdvanceBookMin = false; + /** * The routing context used to actually carry out this search. It is important to build States from TraverseOptions * rather than RoutingContexts,and just keep a reference to the context in the TraverseOptions, rather than using * RoutingContexts for everything because in some testing and graph building situations we need to build a bunch of * initial states with different times and vertices from a single TraverseOptions, without setting all the transit * context or building temporary vertices (with all the exception-throwing checks that entails). - * + * * While they are conceptually separate, TraverseOptions does maintain a reference to its accompanying * RoutingContext (and vice versa) so that both do not need to be passed/injected separately into tight inner loops * within routing algorithms. These references should be set to null when the request scope is torn down -- the @@ -457,7 +578,7 @@ public class RoutingRequest implements Cloneable, Serializable { /** A transit stop that this trip must start from */ public FeedScopedId startingTransitStopId; - + /** A trip where this trip must start from (depart-onboard routing) */ public FeedScopedId startingTransitTripId; @@ -490,7 +611,7 @@ public class RoutingRequest implements Cloneable, Serializable { public boolean longDistance = false; /** Should traffic congestion be considered when driving? */ - public boolean useTraffic = true; + public boolean useTraffic = false; /** The function that compares paths converging on the same vertex to decide which ones continue to be explored. */ public DominanceFunction dominanceFunction = new DominanceFunction.Pareto(); @@ -566,6 +687,18 @@ public class RoutingRequest implements Cloneable, Serializable { // units are in milliseconds public long searchTimeout = -1; + /** + * How many extra ServiceDays to look in the future (or back, if arriveBy=true) + * + * This parameter allows the configuration of how far, in service days, OTP should look for + * transit service when evaluating the next departure (or arrival) at a stop. In some cases, + * for example for services which run weekly or monthly, it may make sense to increase this + * value. Larger values will increase the search time. This does not affect a case where a + * trip starts multiple service days in the past (e.g. a multiday ferry trip will not be + * board-able after the 2nd day in the current implementation). + */ + public int serviceDayLookout = 1; + /** Which path comparator to use */ public String pathComparator = null; @@ -590,12 +723,44 @@ public class RoutingRequest implements Cloneable, Serializable { */ public double weight = 105; + /** + * This parameter is used in GTFS-Flex routing. Preliminary searches before the main search + * need to be able to discover TransitStops in order to create call-and-ride legs which allow + * transfers to fixed-route services. + */ + public boolean enterStationsWithCar = false; + + /** + * Minimum length in meters of partial hop edges. This parameter only applies to GTFS-Flex + * routing, which must be explicitly turned on via the useFlexService parameter in router- + * config.json. + * + * Flag stop and deviated-route service require creating partial PatternHops from points along + * the route to a scheduled stop. This parameter provides a minimum length of such partial + * hops, in order to reduce the amount of hops created when they redundant with regular + * service. + */ + public int flexMinPartialHopLength = 400; + /** Saves split edge which can be split on origin/destination search * * This is used so that TrivialPathException is thrown if origin and destination search would split the same edge */ private StreetEdge splitEdge = null; + /** + * Keep track of epoch time the request was created by OTP. This is currently only used by the + * GTFS-Flex implementation. + * + * In GTFS-Flex, deviated-route and call-and-ride service can define a trip-level parameter + * `drt_advance_book_min`, which determines how far in advance the flexible segment must be + * scheduled. If `flexIgnoreDrtAdvanceBookMin = false`, OTP will only provide itineraries which + * are feasible based on that constraint. For example, if the current time is 1:00pm and a + * particular service must be scheduled one hour in advance, the earliest time the service + * is usable is 2:00pm. + */ + public long clockTimeSec; + /* CONSTRUCTORS */ /** Constructor for options; modes defaults to walk and transit */ @@ -712,8 +877,6 @@ public void setWheelchairAccessible(boolean wheelchairAccessible) { this.wheelchairAccessible = wheelchairAccessible; } - - /** * only allow traversal by the specified mode; don't allow walking bikes. This is used during contraction to reduce the number of possible paths. */ @@ -744,7 +907,7 @@ public T getExtension(Object key) { public IntersectionTraversalCostModel getIntersectionTraversalCostModel() { return traversalCostModel; } - + /** @return the (soft) maximum walk distance */ // If transit is not to be used and this is a point to point search // or one with soft walk limiting, disable walk limit. @@ -752,10 +915,10 @@ public double getMaxWalkDistance() { if (modes.isTransit() || (batch && !softWalkLimiting)) { return maxWalkDistance; } else { - return Double.MAX_VALUE; + return Double.MAX_VALUE; } } - + public void setWalkBoardCost(int walkBoardCost) { if (walkBoardCost < 0) { this.walkBoardCost = 0; @@ -764,7 +927,7 @@ public void setWalkBoardCost(int walkBoardCost) { this.walkBoardCost = walkBoardCost; } } - + public void setBikeBoardCost(int bikeBoardCost) { if (bikeBoardCost < 0) { this.bikeBoardCost = 0; @@ -773,7 +936,7 @@ public void setBikeBoardCost(int bikeBoardCost) { this.bikeBoardCost = bikeBoardCost; } } - + public void setPreferredAgencies(String s) { if (!s.isEmpty()) { preferredAgencies = new HashSet<>(); @@ -789,7 +952,7 @@ public void setPreferredRoutes(String s) { preferredRoutes = RouteMatcher.emptyMatcher(); } } - + public void setOtherThanPreferredRoutesPenalty(int penalty) { if(penalty < 0) penalty = 0; this.otherThanPreferredRoutesPenalty = penalty; @@ -912,21 +1075,21 @@ public void setDateTime(String date, String time, TimeZone tz) { // Fix the date with the provided strategy. LOG.warn("Could not parse date/time. Attempting invalid date strategy: {}", invalidDateStrategy); switch (invalidDateStrategy.toUpperCase()) { - case "USE_CURRENT": - // Attempt to use provided time. - Date now = new Date(); - date = new SimpleDateFormat("yyyy-MM-dd").format(now); - dateObject = DateUtils.toDate(date, time, tz); - if (dateObject == null) { - // Time didn't parse. Use current time instead. - LOG.warn("Couldn't parse time. Using current time instead."); - dateObject = now; - } - break; - // TODO: Add other strategies? For example, guess the nearest date to the one provided. - default: - // If invalidDateStrategy is not one of the above - throw new IllegalArgumentException("Date or time parameter is invalid."); + case "USE_CURRENT": + // Attempt to use provided time. + Date now = new Date(); + date = new SimpleDateFormat("yyyy-MM-dd").format(now); + dateObject = DateUtils.toDate(date, time, tz); + if (dateObject == null) { + // Time didn't parse. Use current time instead. + LOG.warn("Couldn't parse time. Using current time instead."); + dateObject = now; + } + break; + // TODO: Add other strategies? For example, guess the nearest date to the one provided. + default: + // If invalidDateStrategy is not one of the above + throw new IllegalArgumentException("Date or time parameter is invalid."); } } } @@ -953,8 +1116,8 @@ public String toString() { public String toString(String sep) { return from + sep + to + sep + getMaxWalkDistance() + sep + getDateTime() + sep - + arriveBy + sep + optimize + sep + modes.getAsStr() + sep - + getNumItineraries(); + + arriveBy + sep + optimize + sep + modes.getAsStr() + sep + + getNumItineraries(); } public void removeMode(TraverseMode mode) { @@ -1052,10 +1215,12 @@ public RoutingRequest reversedClone() { return ret; } - public void setRoutingContext(Graph graph) { + // Set routing context with passed-in set of temporary vertices. Needed for intermediate places + // as a consequence of the check that temporary vertices are request-specific. + public void setRoutingContext(Graph graph, Collection temporaryVertices) { if (rctx == null) { // graphService.getGraph(routerId) - this.rctx = new RoutingContext(this, graph); + this.rctx = new RoutingContext(this, graph, temporaryVertices); // check after back reference is established, to allow temp edge cleanup on exceptions this.rctx.check(); } else { @@ -1069,6 +1234,10 @@ public void setRoutingContext(Graph graph) { } } + public void setRoutingContext(Graph graph) { + setRoutingContext(graph, null); + } + /** For use in tests. Force RoutingContext to specific vertices rather than making temp edges. */ public void setRoutingContext(Graph graph, Edge fromBackEdge, Vertex from, Vertex to) { // normally you would want to tear down the routing context... @@ -1120,68 +1289,79 @@ public boolean equals(Object o) { } } else { endpointsMatch = ((from == null && other.from == null) || from.equals(other.from)) - && ((to == null && other.to == null) || to.equals(other.to)); + && ((to == null && other.to == null) || to.equals(other.to)); } return endpointsMatch - && dateTime == other.dateTime - && arriveBy == other.arriveBy - && numItineraries == other.numItineraries // should only apply in non-batch? - && walkSpeed == other.walkSpeed - && bikeSpeed == other.bikeSpeed - && carSpeed == other.carSpeed - && maxWeight == other.maxWeight - && worstTime == other.worstTime - && maxTransfers == other.maxTransfers - && modes.equals(other.modes) - && wheelchairAccessible == other.wheelchairAccessible - && optimize.equals(other.optimize) - && maxWalkDistance == other.maxWalkDistance - && maxTransferWalkDistance == other.maxTransferWalkDistance - && maxPreTransitTime == other.maxPreTransitTime - && transferPenalty == other.transferPenalty - && maxSlope == other.maxSlope - && walkReluctance == other.walkReluctance - && waitReluctance == other.waitReluctance - && waitAtBeginningFactor == other.waitAtBeginningFactor - && walkBoardCost == other.walkBoardCost - && bikeBoardCost == other.bikeBoardCost - && bannedRoutes.equals(other.bannedRoutes) - && bannedTrips.equals(other.bannedTrips) - && preferredRoutes.equals(other.preferredRoutes) - && unpreferredRoutes.equals(other.unpreferredRoutes) - && transferSlack == other.transferSlack - && boardSlack == other.boardSlack - && alightSlack == other.alightSlack - && nonpreferredTransferPenalty == other.nonpreferredTransferPenalty - && otherThanPreferredRoutesPenalty == other.otherThanPreferredRoutesPenalty - && useUnpreferredRoutesPenalty == other.useUnpreferredRoutesPenalty - && triangleSafetyFactor == other.triangleSafetyFactor - && triangleSlopeFactor == other.triangleSlopeFactor - && triangleTimeFactor == other.triangleTimeFactor - && stairsReluctance == other.stairsReluctance - && elevatorBoardTime == other.elevatorBoardTime - && elevatorBoardCost == other.elevatorBoardCost - && elevatorHopTime == other.elevatorHopTime - && elevatorHopCost == other.elevatorHopCost - && bikeSwitchTime == other.bikeSwitchTime - && bikeSwitchCost == other.bikeSwitchCost - && bikeRentalPickupTime == other.bikeRentalPickupTime - && bikeRentalPickupCost == other.bikeRentalPickupCost - && bikeRentalDropoffTime == other.bikeRentalDropoffTime - && bikeRentalDropoffCost == other.bikeRentalDropoffCost - && useBikeRentalAvailabilityInformation == other.useBikeRentalAvailabilityInformation - && extensions.equals(other.extensions) - && clampInitialWait == other.clampInitialWait - && reverseOptimizeOnTheFly == other.reverseOptimizeOnTheFly - && ignoreRealtimeUpdates == other.ignoreRealtimeUpdates - && disableRemainingWeightHeuristic == other.disableRemainingWeightHeuristic - && Objects.equal(startingTransitTripId, other.startingTransitTripId) - && useTraffic == other.useTraffic - && disableAlertFiltering == other.disableAlertFiltering - && geoidElevation == other.geoidElevation - && invalidDateStrategy.equals(other.invalidDateStrategy) - && minTransitDistance == other.minTransitDistance - && searchTimeout == other.searchTimeout; + && dateTime == other.dateTime + && arriveBy == other.arriveBy + && numItineraries == other.numItineraries // should only apply in non-batch? + && walkSpeed == other.walkSpeed + && bikeSpeed == other.bikeSpeed + && carSpeed == other.carSpeed + && maxWeight == other.maxWeight + && worstTime == other.worstTime + && maxTransfers == other.maxTransfers + && modes.equals(other.modes) + && wheelchairAccessible == other.wheelchairAccessible + && optimize.equals(other.optimize) + && maxWalkDistance == other.maxWalkDistance + && maxTransferWalkDistance == other.maxTransferWalkDistance + && maxPreTransitTime == other.maxPreTransitTime + && transferPenalty == other.transferPenalty + && maxSlope == other.maxSlope + && walkReluctance == other.walkReluctance + && waitReluctance == other.waitReluctance + && waitAtBeginningFactor == other.waitAtBeginningFactor + && walkBoardCost == other.walkBoardCost + && bikeBoardCost == other.bikeBoardCost + && bannedRoutes.equals(other.bannedRoutes) + && bannedTrips.equals(other.bannedTrips) + && preferredRoutes.equals(other.preferredRoutes) + && unpreferredRoutes.equals(other.unpreferredRoutes) + && transferSlack == other.transferSlack + && boardSlack == other.boardSlack + && alightSlack == other.alightSlack + && nonpreferredTransferPenalty == other.nonpreferredTransferPenalty + && otherThanPreferredRoutesPenalty == other.otherThanPreferredRoutesPenalty + && useUnpreferredRoutesPenalty == other.useUnpreferredRoutesPenalty + && triangleSafetyFactor == other.triangleSafetyFactor + && triangleSlopeFactor == other.triangleSlopeFactor + && triangleTimeFactor == other.triangleTimeFactor + && stairsReluctance == other.stairsReluctance + && elevatorBoardTime == other.elevatorBoardTime + && elevatorBoardCost == other.elevatorBoardCost + && elevatorHopTime == other.elevatorHopTime + && elevatorHopCost == other.elevatorHopCost + && bikeSwitchTime == other.bikeSwitchTime + && bikeSwitchCost == other.bikeSwitchCost + && bikeRentalPickupTime == other.bikeRentalPickupTime + && bikeRentalPickupCost == other.bikeRentalPickupCost + && bikeRentalDropoffTime == other.bikeRentalDropoffTime + && bikeRentalDropoffCost == other.bikeRentalDropoffCost + && useBikeRentalAvailabilityInformation == other.useBikeRentalAvailabilityInformation + && extensions.equals(other.extensions) + && clampInitialWait == other.clampInitialWait + && reverseOptimizeOnTheFly == other.reverseOptimizeOnTheFly + && ignoreRealtimeUpdates == other.ignoreRealtimeUpdates + && disableRemainingWeightHeuristic == other.disableRemainingWeightHeuristic + && Objects.equal(startingTransitTripId, other.startingTransitTripId) + && disableAlertFiltering == other.disableAlertFiltering + && geoidElevation == other.geoidElevation + && invalidDateStrategy.equals(other.invalidDateStrategy) + && minTransitDistance == other.minTransitDistance + && searchTimeout == other.searchTimeout + && flexFlagStopExtraPenalty == other.flexFlagStopExtraPenalty + && flexDeviatedRouteExtraPenalty == other.flexDeviatedRouteExtraPenalty + && flexCallAndRideReluctance == other.flexCallAndRideReluctance + && flexReduceCallAndRideSeconds == other.flexReduceCallAndRideSeconds + && flexReduceCallAndRideRatio == other.flexReduceCallAndRideRatio + && flexFlagStopBufferSize == other.flexFlagStopBufferSize + && flexUseReservationServices == other.flexUseReservationServices + && flexUseEligibilityServices == other.flexUseEligibilityServices + && flexIgnoreDrtAdvanceBookMin == other.flexIgnoreDrtAdvanceBookMin + && flexMinPartialHopLength == other.flexMinPartialHopLength + && clockTimeSec == other.clockTimeSec + && serviceDayLookout == other.serviceDayLookout; } /** @@ -1191,27 +1371,43 @@ public boolean equals(Object o) { @Override public int hashCode() { int hashCode = new Double(walkSpeed).hashCode() + new Double(bikeSpeed).hashCode() - + new Double(carSpeed).hashCode() + new Double(maxWeight).hashCode() - + (int) (worstTime & 0xffffffff) + modes.hashCode() - + (arriveBy ? 8966786 : 0) + (wheelchairAccessible ? 731980 : 0) - + optimize.hashCode() + new Double(maxWalkDistance).hashCode() - + new Double(maxTransferWalkDistance).hashCode() - + new Double(transferPenalty).hashCode() + new Double(maxSlope).hashCode() - + new Double(walkReluctance).hashCode() + new Double(waitReluctance).hashCode() - + new Double(waitAtBeginningFactor).hashCode() * 15485863 - + walkBoardCost + bikeBoardCost + bannedRoutes.hashCode() - + bannedTrips.hashCode() * 1373 + transferSlack * 20996011 - + (int) nonpreferredTransferPenalty + (int) transferPenalty * 163013803 - + new Double(triangleSafetyFactor).hashCode() * 195233277 - + new Double(triangleSlopeFactor).hashCode() * 136372361 - + new Double(triangleTimeFactor).hashCode() * 790052899 - + new Double(stairsReluctance).hashCode() * 315595321 - + maxPreTransitTime * 63061489 - + new Long(clampInitialWait).hashCode() * 209477 - + new Boolean(reverseOptimizeOnTheFly).hashCode() * 95112799 - + new Boolean(ignoreRealtimeUpdates).hashCode() * 154329 - + new Boolean(disableRemainingWeightHeuristic).hashCode() * 193939 - + new Boolean(useTraffic).hashCode() * 10169; + + new Double(carSpeed).hashCode() + new Double(maxWeight).hashCode() + + (int) (worstTime & 0xffffffff) + modes.hashCode() + + (arriveBy ? 8966786 : 0) + (wheelchairAccessible ? 731980 : 0) + + optimize.hashCode() + new Double(maxWalkDistance).hashCode() + + new Double(maxTransferWalkDistance).hashCode() + + new Double(transferPenalty).hashCode() + new Double(maxSlope).hashCode() + + new Double(walkReluctance).hashCode() + new Double(waitReluctance).hashCode() + + new Double(waitAtBeginningFactor).hashCode() * 15485863 + + walkBoardCost + bikeBoardCost + bannedRoutes.hashCode() + + bannedTrips.hashCode() * 1373 + transferSlack * 20996011 + + (int) nonpreferredTransferPenalty + (int) transferPenalty * 163013803 + + new Double(triangleSafetyFactor).hashCode() * 195233277 + + new Double(triangleSlopeFactor).hashCode() * 136372361 + + new Double(triangleTimeFactor).hashCode() * 790052899 + + new Double(stairsReluctance).hashCode() * 315595321 + + maxPreTransitTime * 63061489 + + new Long(clampInitialWait).hashCode() * 209477 + + new Boolean(reverseOptimizeOnTheFly).hashCode() * 95112799 + + new Boolean(ignoreRealtimeUpdates).hashCode() * 154329 + + new Boolean(disableRemainingWeightHeuristic).hashCode() * 193939 + + new Boolean(useTraffic).hashCode() * 10169 + + Integer.hashCode(flexFlagStopExtraPenalty) * 179424691 + + Integer.hashCode(flexDeviatedRouteExtraPenalty) * 7424299 + + Double.hashCode(flexCallAndRideReluctance) * 86666621 + + Integer.hashCode(flexMaxCallAndRideSeconds) * 9994393 + + Integer.hashCode(flexReduceCallAndRideSeconds) * 92356763 + + Double.hashCode(flexReduceCallAndRideRatio) * 171157957 + + Double.hashCode(flexFlagStopBufferSize) * 803989 + + Boolean.hashCode(flexUseReservationServices) * 92429033 + + Boolean.hashCode(flexUseEligibilityServices) * 7916959 + + Boolean.hashCode(flexIgnoreDrtAdvanceBookMin) * 179992387 + + Integer.hashCode(flexMinPartialHopLength) * 15485863 + + Long.hashCode(clockTimeSec) * 833389 + + new Boolean(disableRemainingWeightHeuristic).hashCode() * 193939 + + new Boolean(useTraffic).hashCode() * 10169 + + Integer.hashCode(serviceDayLookout) * 31558519; + if (batch) { hashCode *= -1; // batch mode, only one of two endpoints matters @@ -1320,7 +1516,7 @@ private String getRouteOrAgencyStr(HashSet strings) { } public void setMaxWalkDistance(double maxWalkDistance) { - if (maxWalkDistance > 0) { + if (maxWalkDistance >= 0) { this.maxWalkDistance = maxWalkDistance; bikeWalkingOptions.maxWalkDistance = maxWalkDistance; } @@ -1402,7 +1598,7 @@ public long preferencesPenaltyForRoute(Route route) { long preferences_penalty = 0; String agencyID = route.getAgency().getId(); if ((preferredRoutes != null && !preferredRoutes.equals(RouteMatcher.emptyMatcher())) || - (preferredAgencies != null && !preferredAgencies.isEmpty())) { + (preferredAgencies != null && !preferredAgencies.isEmpty())) { boolean isPreferedRoute = preferredRoutes != null && preferredRoutes.matches(route); boolean isPreferedAgency = preferredAgencies != null && preferredAgencies.contains(agencyID); if (!isPreferedRoute && !isPreferedAgency) { @@ -1474,6 +1670,14 @@ public void canSplitEdge(StreetEdge edge) { } + public void resetClockTime() { + this.clockTimeSec = System.currentTimeMillis() / 1000; + } + + public void setServiceDayLookout(int serviceDayLookout) { + this.serviceDayLookout = serviceDayLookout; + } + public Comparator getPathComparator(boolean compareStartTimes) { if ("duration".equals(pathComparator)) { return new DurationComparator(); diff --git a/src/main/java/org/opentripplanner/routing/core/ServiceDay.java b/src/main/java/org/opentripplanner/routing/core/ServiceDay.java index 052b3d2d3db..0667a95c2f9 100644 --- a/src/main/java/org/opentripplanner/routing/core/ServiceDay.java +++ b/src/main/java/org/opentripplanner/routing/core/ServiceDay.java @@ -46,6 +46,11 @@ public ServiceDay(Graph graph, ServiceDate serviceDate, CalendarService cs, Stri init(graph, cs, timeZone); } + public ServiceDay(Graph graph, ServiceDate serviceDate, CalendarService cs, TimeZone timeZone) { + this.serviceDate = new ServiceDate(serviceDate); + init(graph, cs, timeZone); + } + private void init(Graph graph, CalendarService cs, TimeZone timeZone) { Date d = serviceDate.getAsDate(timeZone); this.midnight = d.getTime() / 1000; diff --git a/src/main/java/org/opentripplanner/routing/core/State.java b/src/main/java/org/opentripplanner/routing/core/State.java index 1620eda465c..4b3d4807a1f 100644 --- a/src/main/java/org/opentripplanner/routing/core/State.java +++ b/src/main/java/org/opentripplanner/routing/core/State.java @@ -28,7 +28,7 @@ public class State implements Cloneable { /* Data which is likely to change at most traversals */ - + // the current time at this state, in milliseconds protected long time; @@ -69,6 +69,8 @@ public class State implements Cloneable { // track the states of all path parsers -- probably changes frequently protected int[] pathParserStates; + int callAndRideTime = 0; + private static final Logger LOG = LoggerFactory.getLogger(State.class); /* CONSTRUCTORS */ @@ -99,7 +101,7 @@ public State(Vertex vertex, long timeSeconds, RoutingRequest options) { // Since you explicitly specify, the vertex, we don't set the backEdge. this(vertex, null, timeSeconds, options); } - + /** * Create an initial state, forcing vertex, back edge and time to the specified values. Useful for reusing * a RoutingContext in TransitIndex, tests, etc. @@ -107,7 +109,7 @@ public State(Vertex vertex, long timeSeconds, RoutingRequest options) { public State(Vertex vertex, Edge backEdge, long timeSeconds, RoutingRequest options) { this(vertex, backEdge, timeSeconds, timeSeconds, options); } - + /** * Create an initial state, forcing vertex, back edge, time and start time to the specified values. Useful for * starting a multiple initial state search, for example when propagating profile results to the street network in @@ -135,7 +137,7 @@ public State(Vertex vertex, Edge backEdge, long timeSeconds, long startTime, Rou } else if (options.bikeParkAndRide) { this.stateData.bikeParked = options.arriveBy; this.stateData.nonTransitMode = this.stateData.bikeParked ? TraverseMode.WALK - : TraverseMode.BICYCLE; + : TraverseMode.BICYCLE; } // if allowed to hail a car, initialize state with CAR mode if the first seen StreetEdge allows cars and a TNC // stop would be allowed there @@ -203,7 +205,7 @@ else if (options.allowVehicleRental) { /** * Create a state editor to produce a child of this state, which will be the result of * traversing the given edge. - * + * * @param e * @return */ @@ -228,7 +230,7 @@ protected State clone() { /** * Retrieve a State extension based on its key. - * + * * @param key - An Object that is a key in this State's extension map * @return - The extension value for the given key, or null if not present */ @@ -241,21 +243,21 @@ public Object getExtension(Object key) { public String toString() { return ""; + + (isBikeRenting() ? "BIKE_RENT " : "") + (isCarParked() ? "CAR_PARKED " : "") + + vertex + ">"; } - + public String toStringVerbose() { - return ""; - } - + return ""; + } + /** Returns time in seconds since epoch */ public long getTimeSeconds() { return time / 1000; @@ -270,7 +272,7 @@ public TripTimes getTripTimes() { return stateData.tripTimes; } - /** + /** * Returns the length of the trip in seconds up to this time, not including the initial wait. * It subtracts out the initial wait, up to a clamp value specified in the request. * If the clamp value is set to -1, no clamping will occur. @@ -285,7 +287,7 @@ public long getActiveTime () { // only subtract up the clamp value if (clampInitialWait >= 0 && initialWait > clampInitialWait) - initialWait = clampInitialWait; + initialWait = clampInitialWait; long activeTime = getElapsedTimeSeconds() - initialWait; @@ -295,7 +297,7 @@ public long getActiveTime () { activeTime = getElapsedTimeSeconds(); } - return activeTime; + return activeTime; } public FeedScopedId getTripId() { @@ -305,7 +307,7 @@ public FeedScopedId getTripId() { public Trip getPreviousTrip() { return stateData.previousTrip; } - + public String getZone() { return stateData.zone; } @@ -357,7 +359,7 @@ public boolean isFinal() { boolean vehicleRentingOk; boolean tncOK = !stateData.opt.useTransportationNetworkCompany || ( isEverBoarded() && - (!isUsingHailedCar() || isTNCStopAllowed()) + (!isUsingHailedCar() || isTNCStopAllowed()) ); if (stateData.opt.arriveBy) { bikeRentingOk = !isBikeRenting(); @@ -395,6 +397,10 @@ public int getPreTransitTime() { return preTransitTime; } + public int getCallAndRideTime() { + return callAndRideTime; + } + public Vertex getVertex() { return this.vertex; } @@ -468,11 +474,11 @@ public boolean isOnboard() { public State getBackState() { return this.backState; } - + public TraverseMode getBackMode () { return stateData.backMode; } - + public boolean isBackWalkingBike () { return stateData.backWalkingBike; } @@ -491,7 +497,7 @@ public String getBackDirection () { return backEdge.getDirection(); } } - + /** * Get the back trip of the given state. For time dependent transit, State will find the * right thing to do. @@ -519,7 +525,7 @@ public long getStartTimeSeconds() { /** * Optional next result that allows {@link Edge} to return multiple results. - * + * * @return the next additional result from an edge traversal, or null if no more results */ public State getNextResult() { @@ -529,18 +535,18 @@ public State getNextResult() { /** * Extend an exiting result chain by appending this result to the existing chain. The usage * model looks like this: - * + * * * TraverseResult result = null; - * + * * for( ... ) { * TraverseResult individualResult = ...; * result = individualResult.addToExistingResultChain(result); * } - * + * * return result; * - * + * * @param existingResultChain the tail of an existing result chain, or null if the chain has not * been started * @return @@ -565,11 +571,11 @@ public RoutingContext getContext() { public RoutingRequest getOptions () { return stateData.opt; } - + /** * This method is on State rather than RoutingRequest because we care whether the user is in * possession of a rented bike. - * + * * @return BICYCLE if routing with an owned bicycle, or if at this state the user is holding on * to a rented bicycle. */ @@ -726,7 +732,7 @@ public boolean multipleOptionsBefore() { } return foundAlternatePaths; } - + public String getPathParserStates() { StringBuilder sb = new StringBuilder(); sb.append("( "); @@ -742,6 +748,10 @@ public TripPattern getLastPattern() { return stateData.lastPattern; } + public boolean isLastBoardAlightDeviated() { + return stateData.isLastBoardAlightDeviated; + } + public ServiceDay getServiceDay() { return stateData.serviceDay; } @@ -759,7 +769,7 @@ public Set getBikeRentalNetworks() { * due to different weights on time-dependent (e.g. transit boarding) edges. If the optimize * parameter is false, the path will be reversed but will have the same duration. This is the * result of combining the functions from GraphPath optimize and reverse. - * + * * @param optimize Should this path be optimized or just reversed? * @param forward Is this an on-the-fly reverse search in the midst of a forward search? * @returns a state at the other end (or this end, in the case of a forward search) @@ -775,21 +785,21 @@ public State optimizeOrReverse (boolean optimize, boolean forward) { while (orig.getBackState() != null) { edge = orig.getBackEdge(); - + if (optimize) { // first board/last alight: figure in wait time in on the fly optimization if (edge instanceof TransitBoardAlight && - forward && - orig.getNumBoardings() == 1 && - ( - // boarding in a forward main search - (((TransitBoardAlight) edge).boarding && - !stateData.opt.arriveBy) || - // alighting in a reverse main search - (!((TransitBoardAlight) edge).boarding && - stateData.opt.arriveBy) - ) - ) { + forward && + orig.getNumBoardings() == 1 && + ( + // boarding in a forward main search + (((TransitBoardAlight) edge).boarding && + !stateData.opt.arriveBy) || + // alighting in a reverse main search + (!((TransitBoardAlight) edge).boarding && + stateData.opt.arriveBy) + ) + ) { ret = ((TransitBoardAlight) edge).traverse(ret, orig.getBackState().getTimeSeconds()); newInitialWaitTime = ret.stateData.initialWaitTime; @@ -798,16 +808,16 @@ public State optimizeOrReverse (boolean optimize, boolean forward) { } if (ret != null && ret.getBackMode() != null && orig.getBackMode() != null && - ret.getBackMode() != orig.getBackMode()) { + ret.getBackMode() != orig.getBackMode()) { ret = ret.next; // Keep the mode the same as on the original graph path (in K+R) } if (ret == null) { LOG.warn("Cannot reverse path at edge: " + edge + ", returning unoptimized " - + "path. If this edge is a PatternInterlineDwell, or if there is a " - + "time-dependent turn restriction here, or if there is no transit leg " - + "in a K+R result, this is not totally unexpected. Otherwise, you " - + "might want to look into it."); + + "path. If this edge is a PatternInterlineDwell, or if there is a " + + "time-dependent turn restriction here, or if there is no transit leg " + + "in a K+R result, this is not totally unexpected. Otherwise, you " + + "might want to look into it."); if (forward) return this; @@ -878,7 +888,7 @@ public State optimizeOrReverse (boolean optimize, boolean forward) { //EdgeNarrative retNarrative = ret.getBackEdgeNarrative(); //copyExistingNarrativeToNewNarrativeAsAppropriate(origNarrative, retNarrative); } - + orig = orig.getBackState(); } @@ -886,10 +896,10 @@ public State optimizeOrReverse (boolean optimize, boolean forward) { State reversed = ret.reverse(); if (getWeight() <= reversed.getWeight()) LOG.warn("Optimization did not decrease weight: before " + this.getWeight() - + " after " + reversed.getWeight()); + + " after " + reversed.getWeight()); if (getElapsedTimeSeconds() != reversed.getElapsedTimeSeconds()) LOG.warn("Optimization changed time: before " + this.getElapsedTimeSeconds() + " after " - + reversed.getElapsedTimeSeconds()); + + reversed.getElapsedTimeSeconds()); if (getActiveTime() <= reversed.getActiveTime()) // NOTE: this can happen and it isn't always bad (i.e. it doesn't always mean that // reverse-opt got called when it shouldn't have). Imagine three lines A, B and C @@ -901,8 +911,8 @@ public State optimizeOrReverse (boolean optimize, boolean forward) { // there is not another possible trip. The waiting time will get pushed towards the // the beginning, but not all the way. LOG.warn("Optimization did not decrease active time: before " - + this.getActiveTime() + " after " + reversed.getActiveTime() - + ", boardings: " + this.getNumBoardings()); + + this.getActiveTime() + " after " + reversed.getActiveTime() + + ", boardings: " + this.getNumBoardings()); if (reversed.getWeight() < this.getBackState().getWeight()) // This is possible; imagine a trip involving three lines, line A, line B and // line C. Lines A and C run hourly while Line B runs every ten minute starting @@ -916,23 +926,23 @@ public State optimizeOrReverse (boolean optimize, boolean forward) { // from a previous state to the beginning of the trip where it is significantly // cheaper. LOG.warn("Weight has been reduced enough to make it run backwards, now:" - + reversed.getWeight() + " backState " + getBackState().getWeight() + ", " - + "number of boardings: " + getNumBoardings()); + + reversed.getWeight() + " backState " + getBackState().getWeight() + ", " + + "number of boardings: " + getNumBoardings()); if (getTimeSeconds() != reversed.getTimeSeconds()) LOG.warn("Times do not match"); if (Math.abs(getWeight() - reversed.getWeight()) > 1 - && newInitialWaitTime == stateData.initialWaitTime) + && newInitialWaitTime == stateData.initialWaitTime) LOG.warn("Weight is changed (before: " + getWeight() + ", after: " - + reversed.getWeight() + "), initial wait times " + "constant at " - + newInitialWaitTime); + + reversed.getWeight() + "), initial wait times " + "constant at " + + newInitialWaitTime); if (newInitialWaitTime != reversed.stateData.initialWaitTime) LOG.warn("Initial wait time not propagated: is " - + reversed.stateData.initialWaitTime + ", should be " + newInitialWaitTime); + + reversed.stateData.initialWaitTime + ", should be " + newInitialWaitTime); // copy the path parser states so this path is not thrown out going forward -// reversed.pathParserStates = -// Arrays.copyOf(this.pathParserStates, this.pathParserStates.length, newLength); - + // reversed.pathParserStates = + // Arrays.copyOf(this.pathParserStates, this.pathParserStates.length, newLength); + // copy things that didn't get copied reversed.initializeFieldsFrom(this); return reversed; @@ -954,14 +964,14 @@ public State optimize() { public State reverse() { return optimizeOrReverse(false, false); } - + /** * After reverse-optimizing, many things are not set. Set them from the unoptimized state. * @param o The other state to initialize things from. */ private void initializeFieldsFrom (State o) { StateData currentStateData = this.stateData; - + // easier to clone and copy back, plus more future proof this.stateData = o.stateData.clone(); this.stateData.initialWaitTime = currentStateData.initialWaitTime; diff --git a/src/main/java/org/opentripplanner/routing/core/StateData.java b/src/main/java/org/opentripplanner/routing/core/StateData.java index 0e618bea5b6..f2d6d627531 100644 --- a/src/main/java/org/opentripplanner/routing/core/StateData.java +++ b/src/main/java/org/opentripplanner/routing/core/StateData.java @@ -24,7 +24,7 @@ public class StateData implements Cloneable { protected TripTimes tripTimes; protected FeedScopedId tripId; - + protected Trip previousTrip; protected double lastTransitWalk = 0; @@ -72,11 +72,13 @@ public class StateData implements Cloneable { protected TripPattern lastPattern; + protected boolean isLastBoardAlightDeviated = false; + protected ServiceDay serviceDay; protected TraverseMode nonTransitMode; - /** + /** * This is the wait time at the beginning of the trip (or at the end of the trip for * reverse searches). In Analyst anyhow, this is is subtracted from total trip length of each * final State in lieu of reverse optimization. It is initially set to zero so that it will be diff --git a/src/main/java/org/opentripplanner/routing/core/StateEditor.java b/src/main/java/org/opentripplanner/routing/core/StateEditor.java index 631e8766e97..164578e55b5 100644 --- a/src/main/java/org/opentripplanner/routing/core/StateEditor.java +++ b/src/main/java/org/opentripplanner/routing/core/StateEditor.java @@ -7,6 +7,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.trippattern.TripTimes; +import org.opentripplanner.routing.vertextype.TemporaryVertex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -107,6 +108,12 @@ public State makeState() { return null; } + // Check TemporaryVertex on a different request + if ((getVertex() instanceof TemporaryVertex) + && !child.getOptions().rctx.temporaryVertices.contains(getVertex())) { + return null; + } + if (child.backState != null) { // make it impossible to use a state with lower weight than its // parent. @@ -227,6 +234,15 @@ public void incrementPreTransitTime(int seconds) { child.preTransitTime += seconds; } + public void incrementCallAndRideTime(int seconds) { + if (seconds < 0) { + LOG.warn("A state's call-n-ride time is being incremented by a negative amount."); + defectiveTraversal = true; + return; + } + child.callAndRideTime += seconds; + } + public void incrementNumBoardings() { cloneStateDataAsNeeded(); child.stateData.numBoardings++; @@ -496,6 +512,10 @@ public int getPreTransitTime() { return child.getPreTransitTime(); } + public int getCallAndRideTime() { + return child.getCallAndRideTime(); + } + public Vertex getVertex() { return child.getVertex(); } @@ -521,6 +541,12 @@ public void setLastPattern(TripPattern pattern) { cloneStateDataAsNeeded(); child.stateData.lastPattern = pattern; } + + public void setIsLastBoardAlightDeviated(boolean isLastBoardAlightDeviated) { + cloneStateDataAsNeeded(); + child.stateData.isLastBoardAlightDeviated = isLastBoardAlightDeviated; + } + public void setOptions(RoutingRequest options) { cloneStateDataAsNeeded(); child.stateData.opt = options; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/AreaEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/AreaEdge.java index bb1734ad22d..acc09a87f06 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/AreaEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/AreaEdge.java @@ -1,6 +1,6 @@ package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.util.I18NString; import org.opentripplanner.util.NonLocalizedString; import org.opentripplanner.routing.vertextype.IntersectionVertex; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/AreaEdgeList.java b/src/main/java/org/opentripplanner/routing/edgetype/AreaEdgeList.java index 2571c1f65f7..bca0ac7abf9 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/AreaEdgeList.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/AreaEdgeList.java @@ -14,13 +14,13 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.vertextype.IntersectionVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.MultiLineString; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.Polygon; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; /** * This is a representation of a set of contiguous OSM areas, used for various tasks related to edge splitting, such as start/endpoint snapping and diff --git a/src/main/java/org/opentripplanner/routing/edgetype/BikeParkEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/BikeParkEdge.java index e8d2adcfb79..61689b6fb5f 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/BikeParkEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/BikeParkEdge.java @@ -7,7 +7,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.vertextype.BikeParkVertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/ElevatorAlightEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/ElevatorAlightEdge.java index e0f0793bfbe..4ee3a8bf115 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/ElevatorAlightEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/ElevatorAlightEdge.java @@ -8,8 +8,8 @@ import org.opentripplanner.routing.vertextype.ElevatorOffboardVertex; import org.opentripplanner.routing.vertextype.ElevatorOnboardVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/ElevatorBoardEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/ElevatorBoardEdge.java index 10fe43d0347..ee1bf918763 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/ElevatorBoardEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/ElevatorBoardEdge.java @@ -8,8 +8,8 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import java.util.Locale; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/ElevatorHopEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/ElevatorHopEdge.java index a49c16ec74a..51e1b3c7404 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/ElevatorHopEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/ElevatorHopEdge.java @@ -7,7 +7,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/FreeEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/FreeEdge.java index a928c23961a..f87d32835b1 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/FreeEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/FreeEdge.java @@ -5,7 +5,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/HopEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/HopEdge.java index fa53170a5ad..68d687eb9f6 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/HopEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/HopEdge.java @@ -2,7 +2,7 @@ import org.opentripplanner.model.Stop; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; /** * FrequencyHops and PatternHops have start/stop Stops @@ -17,4 +17,5 @@ public interface HopEdge { void setGeometry(LineString geometry); + String getFeedId(); } diff --git a/src/main/java/org/opentripplanner/routing/edgetype/IntersectionTransitLink.java b/src/main/java/org/opentripplanner/routing/edgetype/IntersectionTransitLink.java index df936048699..32a8368e0fa 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/IntersectionTransitLink.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/IntersectionTransitLink.java @@ -1,8 +1,8 @@ package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/LegSwitchingEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/LegSwitchingEdge.java index f46ade8ec00..eca312f62a6 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/LegSwitchingEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/LegSwitchingEdge.java @@ -6,7 +6,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/NamedArea.java b/src/main/java/org/opentripplanner/routing/edgetype/NamedArea.java index 861b08105b5..45f7d07b71e 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/NamedArea.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/NamedArea.java @@ -2,7 +2,7 @@ import java.io.Serializable; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; import org.opentripplanner.util.I18NString; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/OnBoardDepartPatternHop.java b/src/main/java/org/opentripplanner/routing/edgetype/OnBoardDepartPatternHop.java index 55e2ec4b7a1..c5c747e0288 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/OnBoardDepartPatternHop.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/OnBoardDepartPatternHop.java @@ -15,8 +15,8 @@ import org.opentripplanner.routing.vertextype.OnboardDepartVertex; import org.opentripplanner.routing.vertextype.PatternStopVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideEdge.java index e8971bb93d7..1350f4e5476 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideEdge.java @@ -8,7 +8,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.vertextype.ParkAndRideVertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideLinkEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideLinkEdge.java index 4f9429dacb5..b8ce12e0450 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideLinkEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/ParkAndRideLinkEdge.java @@ -11,8 +11,8 @@ import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.vertextype.ParkAndRideVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/PartialStreetEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/PartialStreetEdge.java index bb45e7a0be0..b1c6a154e51 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/PartialStreetEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/PartialStreetEdge.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.TurnRestriction; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; @@ -31,7 +31,7 @@ public class PartialStreetEdge extends StreetWithElevationEdge { * The elevation data is calculated using the 'parentEdge' and given 'length'. */ public PartialStreetEdge(StreetEdge parentEdge, StreetVertex v1, StreetVertex v2, - LineString geometry, I18NString name, double length) { + LineString geometry, I18NString name, double length) { super(v1, v2, geometry, name, length, parentEdge.getPermission(), parentEdge.isBack()); this.parentEdge = parentEdge; @@ -123,16 +123,16 @@ public StreetEdge getParentEdge() { @Override public String toString() { return "PartialStreetEdge(" + this.getName() + ", " + this.getFromVertex() + " -> " - + this.getToVertex() + " length=" + this.getDistance() + " carSpeed=" - + this.getCarSpeed() + " parentEdge=" + parentEdge + ")"; + + this.getToVertex() + " length=" + this.getDistance() + " carSpeed=" + + this.getCarSpeed() + " parentEdge=" + parentEdge + ")"; } private void setElevationProfileUsingParents() { setElevationProfile( - ElevationUtils.getPartialElevationProfile( - getParentEdge().getElevationProfile(), 0, getDistance() - ), - false + ElevationUtils.getPartialElevationProfile( + getParentEdge().getElevationProfile(), 0, getDistance() + ), + false ); } } diff --git a/src/main/java/org/opentripplanner/routing/edgetype/PathwayEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/PathwayEdge.java index 5b783116840..e16bc668ec1 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/PathwayEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/PathwayEdge.java @@ -6,8 +6,8 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.routing.core.TraverseMode; import java.util.Locale; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/PatternDwell.java b/src/main/java/org/opentripplanner/routing/edgetype/PatternDwell.java index 5f719c64a6e..1e3cb33a62e 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/PatternDwell.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/PatternDwell.java @@ -10,7 +10,7 @@ import org.opentripplanner.routing.vertextype.PatternArriveVertex; import org.opentripplanner.routing.vertextype.PatternDepartVertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/PatternHop.java b/src/main/java/org/opentripplanner/routing/edgetype/PatternHop.java index 92d641cfd0d..64cb8fe9e6c 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/PatternHop.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/PatternHop.java @@ -1,6 +1,8 @@ package org.opentripplanner.routing.edgetype; import java.util.Locale; + +import org.locationtech.jts.geom.Point; import org.opentripplanner.model.Stop; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; @@ -11,8 +13,8 @@ import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.trippattern.TripTimes; import org.opentripplanner.routing.vertextype.PatternStopVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; /** * A transit vehicle's journey between departure at one stop and arrival at the next. @@ -20,7 +22,7 @@ */ public class PatternHop extends TablePatternEdge implements OnboardEdge, HopEdge { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; private Stop begin, end; @@ -28,17 +30,29 @@ public class PatternHop extends TablePatternEdge implements OnboardEdge, HopEdge private LineString geometry = null; - public PatternHop(PatternStopVertex from, PatternStopVertex to, Stop begin, Stop end, int stopIndex) { + protected PatternHop(PatternStopVertex from, PatternStopVertex to, Stop begin, Stop end, int stopIndex, boolean setInPattern) { super(from, to); this.begin = begin; this.end = end; this.stopIndex = stopIndex; - getPattern().setPatternHop(stopIndex, this); + if (setInPattern) { + getPattern().setPatternHop(stopIndex, this); + } } + public PatternHop(PatternStopVertex from, PatternStopVertex to, Stop begin, Stop end, int stopIndex) { + this(from, to, begin, end, stopIndex, true); + } + + // made more accurate public double getDistance() { - return SphericalDistanceLibrary.distance(begin.getLat(), begin.getLon(), end.getLat(), - end.getLon()); + double distance = 0; + LineString line = getGeometry(); + for (int i = 0; i < line.getNumPoints() - 1; i++) { + Point p0 = line.getPointN(i), p1 = line.getPointN(i+1); + distance += SphericalDistanceLibrary.distance(p0.getCoordinate(), p1.getCoordinate()); + } + return distance; } public TraverseMode getMode() { @@ -64,8 +78,8 @@ public State optimisticTraverse(State state0) { return null; } } - - int runningTime = getPattern().scheduledTimetable.getBestRunningTime(stopIndex); + + int runningTime = (int) timeLowerBound(options); StateEditor s1 = state0.edit(this); s1.incrementTimeInSeconds(runningTime); s1.setBackMode(getMode()); @@ -77,15 +91,21 @@ public State optimisticTraverse(State state0) { public double timeLowerBound(RoutingRequest options) { return getPattern().scheduledTimetable.getBestRunningTime(stopIndex); } - + @Override public double weightLowerBound(RoutingRequest options) { return timeLowerBound(options); } - + + @Override public State traverse(State s0) { + return traverse(s0, s0.edit(this)); + } + + public State traverse(State s0, StateEditor s1) { + RoutingRequest options = s0.getOptions(); - + // Ignore this edge if either of its stop is banned hard if (!options.bannedStopsHard.isEmpty()) { if (options.bannedStopsHard.matches(((PatternStopVertex) fromv).getStop()) @@ -93,21 +113,30 @@ public State traverse(State s0) { return null; } } - - TripTimes tripTimes = s0.getTripTimes(); - int runningTime = tripTimes.getRunningTime(stopIndex); - StateEditor s1 = s0.edit(this); + + int runningTime = getRunningTime(s0); + s1.incrementTimeInSeconds(runningTime); if (s0.getOptions().arriveBy) s1.setZone(getBeginStop().getZoneId()); else s1.setZone(getEndStop().getZoneId()); //s1.setRoute(pattern.getExemplar().route.getId()); - s1.incrementWeight(runningTime); + s1.incrementWeight(getWeight(s0, runningTime)); s1.setBackMode(getMode()); return s1.makeState(); } + public int getRunningTime(State s0) { + TripTimes tripTimes = s0.getTripTimes(); + return tripTimes.getRunningTime(stopIndex); + } + + // allow subclasses to add a weight + public int getWeight(State s0, int runningTime) { + return runningTime; + } + public void setGeometry(LineString geometry) { this.geometry = geometry; } @@ -133,10 +162,23 @@ public Stop getBeginStop() { return begin; } + @Override + public String getFeedId() { + // stops don't really have an agency id, they have the per-feed default id + return begin.getId().getAgencyId(); + } + public String toString() { return "PatternHop(" + getFromVertex() + ", " + getToVertex() + ")"; } + /** + * Return true if any GTFS-Flex service is defined for this hop. + */ + public boolean hasFlexService() { + return false; + } + @Override public int getStopIndex() { return stopIndex; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/PatternInterlineDwell.java b/src/main/java/org/opentripplanner/routing/edgetype/PatternInterlineDwell.java index 3c55eead8c9..7b16b921513 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/PatternInterlineDwell.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/PatternInterlineDwell.java @@ -16,7 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/RentABikeAbstractEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/RentABikeAbstractEdge.java index ff25cf29d64..9efa0ab75f4 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/RentABikeAbstractEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/RentABikeAbstractEdge.java @@ -12,7 +12,7 @@ import org.opentripplanner.routing.vertextype.BikeRentalStationVertex; import com.google.common.collect.Sets; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/RentACarAbstractEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/RentACarAbstractEdge.java index 6c9650b3687..89778c55722 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/RentACarAbstractEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/RentACarAbstractEdge.java @@ -14,7 +14,7 @@ the License, or (at your option) any later version. package org.opentripplanner.routing.edgetype; import com.google.common.collect.Sets; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.routing.car_rental.CarRentalStation; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/RentAVehicleAbstractEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/RentAVehicleAbstractEdge.java index 1d7bfe0e495..59807965cbf 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/RentAVehicleAbstractEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/RentAVehicleAbstractEdge.java @@ -14,7 +14,7 @@ the License, or (at your option) any later version. package org.opentripplanner.routing.edgetype; import com.google.common.collect.Sets; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.vehicle_rental.VehicleRentalStation; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/SemiPermanentPartialStreetEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/SemiPermanentPartialStreetEdge.java index 2cc8a9387f1..950f4f883aa 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/SemiPermanentPartialStreetEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/SemiPermanentPartialStreetEdge.java @@ -1,6 +1,6 @@ package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.model.P2; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/SimpleTransfer.java b/src/main/java/org/opentripplanner/routing/edgetype/SimpleTransfer.java index 39d8786bdcd..227e1581f1d 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/SimpleTransfer.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/SimpleTransfer.java @@ -7,7 +7,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.vertextype.TransitStop; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.List; import java.util.Locale; @@ -48,6 +48,10 @@ public State traverse(State s0) { if(distance > s0.getOptions().maxTransferWalkDistance) { return null; } + // Don't allow SimpleTransfer right after a call-and-ride or deviated-route dropoff - in that case we need to transfer at the same stop + if (s0.isLastBoardAlightDeviated()) { + return null; + } // Only transfer right after riding a vehicle. RoutingRequest rr = s0.getOptions(); double walkspeed = rr.walkSpeed; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StationStopEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/StationStopEdge.java index 85642c5514d..7569eeb1bf3 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StationStopEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StationStopEdge.java @@ -7,7 +7,7 @@ import org.opentripplanner.routing.vertextype.TransitStation; import org.opentripplanner.routing.vertextype.TransitStop; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeParkLink.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeParkLink.java index bd49249b866..b7226368709 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeParkLink.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeParkLink.java @@ -10,8 +10,8 @@ import org.opentripplanner.routing.vertextype.BikeParkVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeRentalLink.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeRentalLink.java index 76a80419c44..48f7fb59e63 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeRentalLink.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetBikeRentalLink.java @@ -9,7 +9,7 @@ import org.opentripplanner.routing.vertextype.BikeRentalStationVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetCarRentalLink.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetCarRentalLink.java index 3a9acaed565..59604f1f9ab 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetCarRentalLink.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetCarRentalLink.java @@ -13,7 +13,6 @@ the License, or (at your option) any later version. package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.LineString; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.StateEditor; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java index 2e2e1fb5a1b..8483ee0a4f4 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java @@ -1,8 +1,8 @@ package org.opentripplanner.routing.edgetype; import com.google.common.collect.Iterables; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.TurnRestriction; import org.opentripplanner.common.TurnRestrictionType; import org.opentripplanner.common.geometry.CompactLineString; @@ -27,7 +27,6 @@ import org.opentripplanner.routing.vertextype.SplitterVertex; import org.opentripplanner.routing.vertextype.StreetVertex; import org.opentripplanner.routing.vertextype.TemporarySplitterVertex; -import org.opentripplanner.traffic.StreetSpeedSnapshot; import org.opentripplanner.util.BitSetUtils; import org.opentripplanner.util.I18NString; import org.opentripplanner.util.NonLocalizedString; @@ -621,18 +620,17 @@ private StateEditor doTraverse(State s0, RoutingRequest options, TraverseMode tr if (traverseMode.equals(TraverseMode.WALK)) { // take slopes into account when walking // FIXME: this causes steep stairs to be avoided. see #1297. - double costs = ElevationUtils.getWalkCostsForSlope(getDistance(), getMaxSlope()); - // as the cost walkspeed is assumed to be for 4.8km/h (= 1.333 m/sec) we need to adjust - // for the walkspeed set by the user - double elevationUtilsSpeed = 4.0 / 3.0; - weight = costs * (elevationUtilsSpeed / speed); + double distance = getSlopeWalkSpeedEffectiveLength(); + weight = distance / speed; weight = weight * walkComfortScore; time = weight; //treat cost as time, as in the current model it actually is the same (this can be checked for maxSlope == 0) /* // debug code if(weight > 100){ - double timeflat = length / speed; - System.out.format("line length: %.1f m, slope: %.3f ---> slope costs: %.1f , weight: %.1f , time (flat): %.1f %n", length, elevationProfile.getMaxSlope(), costs, weight, timeflat); + double timeflat = length_mm / speed; + + + System.out.format("line length: %.1f m, slope: %.3f ---> distance: %.1f , weight: %.1f , time (flat): %.1f %n", getDistance(), getMaxSlope(), distance, weight, timeflat); } */ } @@ -668,6 +666,8 @@ private StateEditor doTraverse(State s0, RoutingRequest options, TraverseMode tr } } + int roundedTime = (int) Math.ceil(time); + /* Compute turn cost. */ StreetEdge backPSE; if (backEdge != null && backEdge instanceof StreetEdge) { @@ -744,8 +744,8 @@ else if ( s1.incrementWalkDistance(realTurnCost / 100); // just a tie-breaker } - long turnTime = (long) Math.ceil(realTurnCost); - time += turnTime; + int turnTime = (int) Math.ceil(realTurnCost); + roundedTime += turnTime; weight += options.turnReluctance * realTurnCost; } @@ -783,7 +783,6 @@ else if ( // either soft or hard. // We can safely assume no limit on driving after transit as most TNC companies will drive // outside of the pickup boundaries. - int roundedTime = (int) Math.ceil(time); if ( options.kissAndRide || options.parkAndRide || @@ -874,18 +873,6 @@ public double calculateSpeed(RoutingRequest options, TraverseMode traverseMode, return Double.NaN; } else if (traverseMode.isDriving()) { // NOTE: Automobiles have variable speeds depending on the edge type - if (options.useTraffic) { - // the expected speed based on traffic - StreetSpeedSnapshot source = options.getRoutingContext().streetSpeedSnapshot; - - if (source != null) { - double congestedSpeed = source.getSpeed(this, traverseMode, timeMillis); - - if (!Double.isNaN(congestedSpeed)) - return congestedSpeed; - } - } - return calculateCarSpeed(options); } else if (traverseMode == TraverseMode.MICROMOBILITY) { return Math.min( @@ -1078,6 +1065,10 @@ public double getSlopeWorkCostEffectiveLength() { return getDistance(); } + public double getSlopeWalkSpeedEffectiveLength() { + return getDistance(); + } + public void setBicycleSafetyFactor(float bicycleSafetyFactor) { this.bicycleSafetyFactor = bicycleSafetyFactor; } @@ -1290,18 +1281,18 @@ public void setSlopeOverride(boolean slopeOverride) { * TODO change everything to clockwise from North */ public int getInAngle() { - return this.inAngle * 180 / 128; + return (int) Math.round(this.inAngle * 180 / 128.0); } /** Return the azimuth of the last segment in this edge in integer degrees clockwise from South. */ public int getOutAngle() { - return this.outAngle * 180 / 128; + return (int) Math.round(this.outAngle * 180 / 128.0); } protected List getTurnRestrictions(Graph graph) { return graph.getTurnRestrictions(this); } - + /** calculate the length of this street segement from its geometry */ protected void calculateLengthFromGeometry () { double accumulatedMeters = 0; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetRentalLink.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetRentalLink.java index a945005395b..241bc0d6528 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetRentalLink.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetRentalLink.java @@ -1,6 +1,6 @@ package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.vertextype.RentalStationVertex; import org.opentripplanner.routing.vertextype.StreetVertex; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetTransitLink.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetTransitLink.java index 164ee656ac6..753a743109e 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetTransitLink.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetTransitLink.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.model.Trip; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.routing.core.RoutingRequest; @@ -30,8 +30,8 @@ public class StreetTransitLink extends Edge { private TransitStop transitStop; public StreetTransitLink(StreetVertex fromv, TransitStop tov, boolean wheelchairAccessible) { - super(fromv, tov); - transitStop = tov; + super(fromv, tov); + transitStop = tov; this.wheelchairAccessible = wheelchairAccessible; } @@ -75,8 +75,10 @@ public State traverse(State s0) { // it is possible that two stops can have the same GPS coordinate thus creating a possibility for a // legitimate StreetTransitLink > StreetTransitLink sequence, so only forbid two StreetTransitLinks to be taken // if they are for the same stop. - if (s0.backEdge instanceof StreetTransitLink && - ((StreetTransitLink) s0.backEdge).transitStop == this.transitStop) { + if ( + s0.backEdge instanceof StreetTransitLink && + ((StreetTransitLink) s0.backEdge).transitStop == this.transitStop + ) { return null; } @@ -85,6 +87,11 @@ public State traverse(State s0) { return null; } + // Do not get off at a real stop when on call-n-ride (force a transfer instead). + if (s0.isLastBoardAlightDeviated() && !(transitStop.checkCallAndRideStreetLinkOk(s0))) { + return null; + } + RoutingRequest req = s0.getOptions(); if (s0.getOptions().wheelchairAccessible && !wheelchairAccessible) { return null; @@ -106,8 +113,7 @@ public State traverse(State s0) { /* Determine if transit should be boarded if currently driving a car */ /* Note that in arriveBy searches this is double-traversing link edges to fork the state into both WALK and CAR mode. This is an insane hack. */ - if (s0.getNonTransitMode() == TraverseMode.CAR) { - // OK to leave car if in Kiss and Ride mode + if (s0.getNonTransitMode() == TraverseMode.CAR && !req.enterStationsWithCar) { if (req.kissAndRide && !s0.isCarParked()) { s1.setCarParked(true); } else if (req.useTransportationNetworkCompany && s0.isUsingHailedCar()) { @@ -199,5 +205,4 @@ public String toString() { return "StreetTransitLink(" + fromv + " -> " + tov + ")"; } - } diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetVehicleRentalLink.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetVehicleRentalLink.java index 1cf6e315a59..4943abec99c 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetVehicleRentalLink.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetVehicleRentalLink.java @@ -13,7 +13,6 @@ the License, or (at your option) any later version. package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.LineString; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.StateEditor; diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetWithElevationEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetWithElevationEdge.java index af1de39c7b7..f7d1b912687 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetWithElevationEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetWithElevationEdge.java @@ -8,13 +8,13 @@ import org.opentripplanner.routing.util.SlopeCosts; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.util.I18NString; import org.opentripplanner.util.NonLocalizedString; /** * A StreetEdge with elevation data. - * + * * @author laurent */ public class StreetWithElevationEdge extends StreetEdge { @@ -44,14 +44,20 @@ public class StreetWithElevationEdge extends StreetEdge { // overestimate of aerodynamic drag. private double maximumDragResistiveForceComponent; + private double effectiveWalkFactor = 1.0; + + /** + * Remember to call the {@link #setElevationProfile(PackedCoordinateSequence, boolean)} to initiate elevation data. + */ public StreetWithElevationEdge(StreetVertex v1, StreetVertex v2, LineString geometry, - I18NString name, double length, StreetTraversalPermission permission, boolean back) { + I18NString name, double length, StreetTraversalPermission permission, boolean back) { super(v1, v2, geometry, name, length, permission, back); + } public StreetWithElevationEdge(StreetVertex v1, StreetVertex v2, LineString geometry, - String name, double length, StreetTraversalPermission permission, boolean back) { - super(v1, v2, geometry, new NonLocalizedString(name), length, permission, back); + String name, double length, StreetTraversalPermission permission, boolean back) { + this(v1, v2, geometry, new NonLocalizedString(name), length, permission, back); } @Override @@ -74,6 +80,7 @@ public boolean setElevationProfile(PackedCoordinateSequence elev, boolean comput slopeWorkFactor = (float)costs.slopeWorkFactor; maxSlope = (float)costs.maxSlope; flattened = costs.flattened; + effectiveWalkFactor = costs.effectiveWalkFactor; bicycleSafetyFactor *= costs.lengthMultiplier; bicycleSafetyFactor += costs.slopeSafetyCost / getDistance(); @@ -126,10 +133,12 @@ public double getSlopeWorkCostEffectiveLength() { @Override public double calculateSpeed(RoutingRequest options, TraverseMode traverseMode, long timeMillis) { // use default StreetEdge method to calculate speed if the traverseMode is not micromobility - if (traverseMode != TraverseMode.MICROMOBILITY) return super.calculateSpeed(options, traverseMode, timeMillis); + if (traverseMode != TraverseMode.MICROMOBILITY) + return super.calculateSpeed(options, traverseMode, timeMillis); // TODO: figure out why this is null sometimes - if (gradients == null) return super.calculateSpeed(options, traverseMode, timeMillis); + if (gradients == null) + return super.calculateSpeed(options, traverseMode, timeMillis); // calculate and accumulate the total travel time and distance it would take to traverse each gradient // these values will eventually be used to calculate an overall average speed. @@ -164,10 +173,18 @@ public double calculateSpeed(RoutingRequest options, TraverseMode traverseMode, return distance / time; } + /** + * The effective walk distance is adjusted to take the elevation into account. + */ + @Override + public double getSlopeWalkSpeedEffectiveLength() { + return effectiveWalkFactor * getDistance(); + } + @Override public String toString() { return "StreetWithElevationEdge(" + getId() + ", " + getName() + ", " + fromv + " -> " - + tov + " length=" + this.getDistance() + " carSpeed=" + this.getCarSpeed() - + " permission=" + this.getPermission() + ")"; + + tov + " length=" + this.getDistance() + " carSpeed=" + this.getCarSpeed() + + " permission=" + this.getPermission() + ")"; } } diff --git a/src/main/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdge.java index 0a9da52b09c..d21131f57f2 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdge.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.edgetype; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.TurnRestriction; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; @@ -10,6 +10,8 @@ import org.opentripplanner.routing.vertextype.TemporaryVertex; import org.opentripplanner.util.I18NString; +import java.util.List; + /** * This class models a StreetEdge that was non-destructively split from another StreetEdge for the purposes of modeling * StreetEdges that should only be valid for a single request. These edges typically include edges used to link the @@ -25,12 +27,95 @@ public TemporaryPartialStreetEdge( double length ) { super(parentEdge, v1, v2, geometry, name, length); - // Assert that the edge is going in the right direction [only possible if vertex is temporary] assertEdgeIsNotDirectedAwayFromTemporaryEndVertex(v1); assertEdgeIsDirectedTowardsTemporaryEndVertex(v2); } + /** + * Partial edges are always partial. + */ + @Override + public boolean isPartial() { + return true; + } + + /** + * Have the ID of their parent. + */ + @Override + public int getId() { + return parentEdge.getId(); + } + + /** + * Have the inbound angle of their parent. + */ + @Override + public int getInAngle() { + return parentEdge.getInAngle(); + } + + /** + * Have the outbound angle of their parent. + */ + @Override + public int getOutAngle() { + return parentEdge.getInAngle(); + } + + /** + * Have the turn restrictions of their parent. + */ + @Override + protected List getTurnRestrictions(Graph graph) { + return graph.getTurnRestrictions(parentEdge); + } + + /** + * This implementation makes it so that TurnRestrictions on the parent edge are applied to this edge as well. + */ + @Override + public boolean isEquivalentTo(Edge e) { + return (e == this || e == parentEdge); + } + + @Override + public boolean isReverseOf(Edge e) { + Edge other = e; + if (e instanceof TemporaryPartialStreetEdge) { + other = ((TemporaryPartialStreetEdge) e).parentEdge; + } + + // TODO(flamholz): is there a case where a partial edge has a reverse of its own? + return parentEdge.isReverseOf(other); + } + + @Override + public boolean isRoundabout() { + return parentEdge.isRoundabout(); + } + + /** + * Returns true if this edge is trivial - beginning and ending at the same point. + */ + public boolean isTrivial() { + Coordinate fromCoord = this.getFromVertex().getCoordinate(); + Coordinate toCoord = this.getToVertex().getCoordinate(); + return fromCoord.equals(toCoord); + } + + public StreetEdge getParentEdge() { + return parentEdge; + } + + @Override + public String toString() { + return "TemporaryPartialStreetEdge(" + this.getName() + ", " + this.getFromVertex() + " -> " + + this.getToVertex() + " length=" + this.getDistance() + " carSpeed=" + + this.getCarSpeed() + " parentEdge=" + parentEdge + ")"; + } + private void assertEdgeIsNotDirectedAwayFromTemporaryEndVertex(StreetVertex v1) { if(v1 instanceof TemporaryVertex) { if (((TemporaryVertex)v1).isEndVertex()) { @@ -47,10 +132,12 @@ private void assertEdgeIsDirectedTowardsTemporaryEndVertex(StreetVertex v2) { } } - @Override - public String toString() { - return "TemporaryPartialStreetEdge(" + this.getName() + ", " + this.getFromVertex() + " -> " - + this.getToVertex() + " length=" + this.getDistance() + " carSpeed=" - + this.getCarSpeed() + " parentEdge=" + parentEdge + ")"; + private void setElevationProfileUsingParents() { + setElevationProfile( + ElevationUtils.getPartialElevationProfile( + getParentEdge().getElevationProfile(), 0, getDistance() + ), + false + ); } } diff --git a/src/main/java/org/opentripplanner/routing/edgetype/TimedTransferEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/TimedTransferEdge.java index 20a6daa8091..143d94efc4b 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/TimedTransferEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/TimedTransferEdge.java @@ -6,7 +6,7 @@ import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/Timetable.java b/src/main/java/org/opentripplanner/routing/edgetype/Timetable.java index 3e6c165d093..cf6135b1ce2 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/Timetable.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/Timetable.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.Arrays; +import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -101,6 +102,8 @@ public Timetable(TripPattern pattern) { * @param bestWait -1 means there is not yet any best known time. */ public boolean temporallyViable(ServiceDay sd, long searchTime, int bestWait, boolean boarding) { + if (this.pattern.services == null) + return true; // Check whether any services are running at all on this pattern. if ( ! sd.anyServiceRunning(this.pattern.services)) return false; // Make the search time relative to the given service day. @@ -118,10 +121,43 @@ public boolean temporallyViable(ServiceDay sd, long searchTime, int bestWait, bo /** * Get the next (previous) trip that departs (arrives) from the specified stop at or after * (before) the specified time. + * + * For GTFS-Flex service, this method will take into account the fact that the passenger + * may be boarding or alighting midway between stops (flag stops), and that the vehicle may + * be deviating off-route to pick up or drop off the passenger (deviated-route service.) In + * both cases, unscheduled timepoints on the route in between scheduled stops are calculated + * via linear interpolation. + * + * The parameters `flexOffsetScale`, `preBoardDirectTime`, and `postAlightDirectTime` are + * specific to GTFS-Flex service. If arrivals/departures are being evaluated for default + * fixed-route service, these parameters will have the value 0 (see + * {@link #getNextTrip(State, ServiceDay, int, boolean)}). + * + * @param s0 State to evaluate the method at; the method uses the state's time. + * @param serviceDay Only consider trips if their service_id is active on this day + * @param stopIndex Index of stop in the trip to evaluate departure or arrival at + * @param boarding If true, find next trip which departs specified stop; if false, find + * previous trip which arrives at specified stop + * @param flexOffsetScale For GTFS-Flex routing, a control parameter to determine the amount of + * seconds to offset the scheduled timepoint due to a board/alight in + * the middle of a + * {@link org.opentripplanner.routing.edgetype.flex.FlexPatternHop}. + * Use the percentage in [0, 1] ([-1, 0]) from the beginning (end) of + * the hop at which the board (alight) is taking place. Note that the + * value is expected to be negative when evaluating alight points. Use + * 0 if this is not a flag stop or deviated-route board (alight) point. + * @param flexPreBoardDirectTime For GTFS-Flex routing, the amount of time the vehicle travels + * before rejoining the route, or 0 if this is not a deviated- + * route board (alight) point. + * @param flexPostAlightDirectTime For GTFS-Flex routing, the amount of time the vehicle + * travels after leaving the route, or 0 if this is not a + * deviated-route board (alight) point. + * * @return the TripTimes object representing the (possibly updated) best trip, or null if no * trip matches both the time and other criteria. */ - public TripTimes getNextTrip(State s0, ServiceDay serviceDay, int stopIndex, boolean boarding) { + public TripTimes getNextTrip(State s0, ServiceDay serviceDay, int stopIndex, boolean boarding, double flexOffsetScale, + int flexPreBoardDirectTime, int flexPostAlightDirectTime) { /* Search at the state's time, but relative to midnight on the given service day. */ int time = serviceDay.secondsSinceMidnight(s0.getTimeSeconds()); // NOTE the time is sometimes negative here. That is fine, we search for the first trip of the day. @@ -150,7 +186,22 @@ public TripTimes getNextTrip(State s0, ServiceDay serviceDay, int stopIndex, boo int adjustedTime = adjustTimeForTransfer(s0, currentStop, tt.trip, boarding, serviceDay, time); if (adjustedTime == -1) continue; if (boarding) { - int depTime = tt.getDepartureTime(stopIndex); + // For GTFS-Flex, if this is a flag-stop or deviated-route board/alight, we need to + // add to the scheduled timepoint the amount of time the vehicle travels along the + // hop before the board/alight, and subtract the amount of time the vehicle travels + // off-route before rejoining the route. Both these values are 0 for regular fixed- + // route board/alights. + int flexTimeAdjustment = 0; + if (flexOffsetScale != 0 || flexPreBoardDirectTime != 0) { + int timeIntoHop = 0; + if (stopIndex + 1 < tt.getNumStops() && flexOffsetScale != 0.0) { + timeIntoHop = (int) Math.round(flexOffsetScale * tt.getRunningTime(stopIndex)); + } + int vehicleTime = (flexPreBoardDirectTime == 0) ? 0 : tt.getDemandResponseMaxTime(flexPreBoardDirectTime); + flexTimeAdjustment = timeIntoHop - vehicleTime; + } + + int depTime = tt.getDepartureTime(stopIndex) + flexTimeAdjustment; if (depTime < 0) continue; // negative values were previously used for canceled trips/passed stops/skipped stops, but // now its not sure if this check should be still in place because there is a boolean field // for canceled trips @@ -159,7 +210,21 @@ public TripTimes getNextTrip(State s0, ServiceDay serviceDay, int stopIndex, boo bestTime = depTime; } } else { - int arvTime = tt.getArrivalTime(stopIndex); + // For GTFS-Flex, subtract from the scheduled timepoint the amount of time left in + // the hop after the vehicle drops off the passenger (note flexOffsetScale < 0 + // in this case), and add the amount of time the vehicle travels off-route before + // the passenger alights. + int flexTimeAdjustment = 0; + if (flexOffsetScale != 0 || flexPostAlightDirectTime != 0) { + int timeIntoHop = 0; + if (stopIndex - 1 >= 0 && flexOffsetScale != 0.0) { + timeIntoHop = (int) Math.round(flexOffsetScale * tt.getRunningTime(stopIndex - 1)); + } + int vehicleTime = (flexPostAlightDirectTime == 0) ? 0 : tt.getDemandResponseMaxTime(flexPostAlightDirectTime); + flexTimeAdjustment = timeIntoHop + vehicleTime; + } + + int arvTime = tt.getArrivalTime(stopIndex) + flexTimeAdjustment; if (arvTime < 0) continue; if (arvTime <= adjustedTime && arvTime > bestTime) { bestTrip = tt; @@ -202,6 +267,68 @@ public TripTimes getNextTrip(State s0, ServiceDay serviceDay, int stopIndex, boo return bestTrip; } + /** + * Get the next (previous) trip that departs (arrives) from the specified stop at or after + * (before) the specified time. + * + * @param s0 State to evaluate the method at; the method uses the state's time. + * @param serviceDay Only consider trips if their service_id is active on this day + * @param stopIndex index of stop in the trip to evaluate departure or arrival at + * @param boarding if true, find next trip which departs specified stop; if false, find + * previous trip which arrives at specified stop + * + * @return the TripTimes object representing the (possibly updated) best trip, or null if no + * trip matches both the time and other criteria. + */ + public TripTimes getNextTrip(State s0, ServiceDay serviceDay, int stopIndex, boolean boarding) { + return getNextTrip(s0, serviceDay, stopIndex, boarding, 0, 0, 0); + } + + // could integrate with getNextTrip + public TripTimes getNextCallNRideTrip(State s0, ServiceDay serviceDay, int stopIndex, boolean boarding, int directTime) { + /* Search at the state's time, but relative to midnight on the given service day. */ + int time = serviceDay.secondsSinceMidnight(s0.getTimeSeconds()); + // NOTE the time is sometimes negative here. That is fine, we search for the first trip of the day. + // Adjust for possible boarding time TODO: This should be included in the trip and based on GTFS + if (boarding) { + time += s0.getOptions().getBoardTime(this.pattern.mode); + } else { + time -= s0.getOptions().getAlightTime(this.pattern.mode); + } + TripTimes bestTrip = null; + Stop currentStop = pattern.getStop(stopIndex); + long bestTime = boarding ? Long.MAX_VALUE : Long.MIN_VALUE; + boolean useClockTime = !s0.getOptions().flexIgnoreDrtAdvanceBookMin; + long clockTime = s0.getOptions().clockTimeSec; + for (TripTimes tt : tripTimes) { + if (tt.isCanceled()) continue; + if ( ! serviceDay.serviceRunning(tt.serviceCode)) continue; // TODO merge into call on next line + if ( ! tt.tripAcceptable(s0, stopIndex)) continue; + int adjustedTime = adjustTimeForTransfer(s0, currentStop, tt.trip, boarding, serviceDay, time); + if (adjustedTime == -1) continue; + if (boarding) { + long depTime = tt.getCallAndRideBoardTime(stopIndex, adjustedTime, directTime, serviceDay, useClockTime, clockTime); + if (depTime >= adjustedTime && depTime < bestTime && inBounds(depTime)) { + bestTrip = tt; + bestTime = depTime; + } + } else { + long arvTime = tt.getCallAndRideAlightTime(stopIndex, adjustedTime, directTime, serviceDay, useClockTime, clockTime); + if (arvTime < 0) continue; + if (arvTime <= adjustedTime && arvTime > bestTime && inBounds(arvTime)) { + bestTrip = tt; + bestTime = arvTime; + } + } + } + + return bestTrip; + } + + private boolean inBounds(long time) { + return time >= minTime && time <= maxTime; + } + /** * Check transfer table rules. Given the last alight time from the State, * return the boarding time t0 adjusted for this particular trip's minimum transfer time, @@ -559,4 +686,12 @@ public void setServiceCodes (Map serviceCodes) { } } + public int getMaxTime() { + return maxTime; + } + + public int getMinTime() { + return minTime; + } + } diff --git a/src/main/java/org/opentripplanner/routing/edgetype/TransferEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/TransferEdge.java index 4579c6970c7..8f9e6586bb8 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/TransferEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/TransferEdge.java @@ -5,7 +5,7 @@ import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.vertextype.TransitStationStop; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; /** diff --git a/src/main/java/org/opentripplanner/routing/edgetype/TransitBoardAlight.java b/src/main/java/org/opentripplanner/routing/edgetype/TransitBoardAlight.java index c980b54d535..306199b2a1b 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/TransitBoardAlight.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/TransitBoardAlight.java @@ -3,6 +3,7 @@ import java.util.BitSet; import java.util.Locale; +import org.opentripplanner.model.Route; import org.opentripplanner.model.Stop; import org.opentripplanner.model.Trip; import org.opentripplanner.routing.core.RoutingContext; @@ -20,8 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.LineString; - +import org.locationtech.jts.geom.LineString; /** * Models boarding or alighting a vehicle - that is to say, traveling from a state off @@ -132,7 +132,15 @@ public State traverse(State s0, long arrivalTimeAtStop) { /* If the user requested a wheelchair accessible trip, check whether and this stop is not accessible. */ if (options.wheelchairAccessible && ! getPattern().wheelchairAccessible(stopIndex)) { return null; - }; + } + + // if eligibility-restricted services are disallowed, check this route. Only supports 0/1 values. + if (!options.flexUseEligibilityServices) { + Route route = getPattern().route; + if (route.hasEligibilityRestricted() && route.getEligibilityRestricted() == 1) { + return null; + } + } /* * Determine whether we are going onto or off of transit. Entering and leaving transit is @@ -160,6 +168,7 @@ public State traverse(State s0, long arrivalTimeAtStop) { // arrives/departs, so previousStop is direction-dependent. s1.setPreviousStop(getStop()); s1.setLastPattern(this.getPattern()); + s1.setIsLastBoardAlightDeviated(isDeviated()); if (boarding) { int boardingTime = options.getBoardTime(this.getPattern().mode); if (boardingTime != 0) { @@ -175,6 +184,7 @@ public State traverse(State s0, long arrivalTimeAtStop) { // TODO: should we have different cost for alighting and boarding compared to regular waiting? } } + s1.incrementWeight(getExtraWeight(options)); /* Determine the wait. */ if (arrivalTimeAtStop > 0) { // FIXME what is this arrivalTimeAtStop? @@ -214,6 +224,7 @@ public State traverse(State s0, long arrivalTimeAtStop) { } s1.setBackMode(getMode()); + return s1.makeState(); } else { /* We are going onto transit and must look for a suitable transit trip on this pattern. */ @@ -243,26 +254,24 @@ public State traverse(State s0, long arrivalTimeAtStop) { * 00:30 tommorrow. The 00:30 trip should be taken, but if we stopped the search after * finding today's 25:00 trip we would never find tomorrow's 00:30 trip. */ - TripPattern tripPattern = this.getPattern(); int bestWait = -1; TripTimes bestTripTimes = null; ServiceDay bestServiceDay = null; for (ServiceDay sd : rctx.serviceDays) { /* Find the proper timetable (updated or original) if there is a realtime snapshot. */ - Timetable timetable = tripPattern.getUpdatedTimetable(options, sd); + Timetable timetable = getPattern().getUpdatedTimetable(options, sd); /* Skip this day/timetable if no trip in it could possibly be useful. */ - // TODO disabled until frequency representation is stable, and min/max timetable times are set from frequencies - // However, experiments seem to show very little measurable improvement here (due to cache locality?) - // if ( ! timetable.temporallyViable(sd, s0.getTimeSeconds(), bestWait, boarding)) continue; - /* Find the next or prev departure depending on final boolean parameter. */ - TripTimes tripTimes = timetable.getNextTrip(s0, sd, stopIndex, boarding); + if ( ! timetable.temporallyViable(sd, s0.getTimeSeconds(), bestWait, boarding)) { + continue; + } + TripTimes tripTimes = getNextTrip(s0, sd, timetable); if (tripTimes != null) { /* Wait is relative to departures on board and arrivals on alight. */ - int wait = boarding ? - (int)(sd.time(tripTimes.getDepartureTime(stopIndex)) - s0.getTimeSeconds()): - (int)(s0.getTimeSeconds() - sd.time(tripTimes.getArrivalTime(stopIndex))); + int wait = calculateWait(s0, sd, tripTimes); /* A trip was found. The wait should be non-negative. */ - if (wait < 0) LOG.error("Negative wait time when boarding."); + if (wait < 0) { + LOG.error("Negative wait time when boarding."); + } /* Track the soonest departure over all relevant schedules. */ if (bestWait < 0 || wait < bestWait) { bestWait = wait; @@ -274,20 +283,6 @@ public State traverse(State s0, long arrivalTimeAtStop) { if (bestWait < 0) return null; // no appropriate trip was found Trip trip = bestTripTimes.trip; - /* Check if route is preferred by the user. */ - long preferences_penalty = options.preferencesPenaltyForRoute(getPattern().route); - - /* Compute penalty for non-preferred transfers. */ - int transferPenalty = 0; - /* If this is not the first boarding, then we are transferring. */ - if (s0.isEverBoarded()) { - TransferTable transferTable = options.getRoutingContext().transferTable; - int transferTime = transferTable.getTransferTime(s0.getPreviousStop(), - getStop(), s0.getPreviousTrip(), trip, boarding); - transferPenalty = transferTable.determineTransferPenalty(transferTime, - options.nonpreferredTransferPenalty); - } - /* Found a trip to board. Now make the child state. */ StateEditor s1 = s0.edit(this); s1.setBackMode(getMode()); @@ -310,9 +305,21 @@ public State traverse(State s0, long arrivalTimeAtStop) { } else { wait_cost *= options.waitReluctance; } - - s1.incrementWeight(preferences_penalty); - s1.incrementWeight(transferPenalty); + + long preferences_penalty = options.preferencesPenaltyForRoute(getPattern().route); + + /* Compute penalty for non-preferred transfers. */ + int transferPenalty = 0; + /* If this is not the first boarding, then we are transferring. */ + if (s0.isEverBoarded()) { + TransferTable transferTable = options.getRoutingContext().transferTable; + int transferTime = transferTable.getTransferTime(s0.getPreviousStop(), + getStop(), s0.getPreviousTrip(), trip, boarding); + transferPenalty = transferTable.determineTransferPenalty(transferTime, + options.nonpreferredTransferPenalty); + } + + s1.incrementWeight(preferences_penalty + transferPenalty); // when reverse optimizing, the board cost needs to be applied on // alight to prevent state domination due to free alights @@ -321,7 +328,9 @@ public State traverse(State s0, long arrivalTimeAtStop) { } else { s1.incrementWeight(wait_cost + options.getBoardCost(s0.getNonTransitMode())); } - + + s1.incrementWeight(getExtraWeight(options)); + // On-the-fly reverse optimization // determine if this needs to be reverse-optimized. // The last alight can be moved forward by bestWait (but no further) without @@ -336,12 +345,30 @@ public State traverse(State s0, long arrivalTimeAtStop) { if (optimized == null) LOG.error("Null optimized state. This shouldn't happen."); return optimized; } - + /* If we didn't return an optimized path, return an unoptimized one. */ return s1.makeState(); } } + public long getExtraWeight(RoutingRequest options) { + return 0; + } + + public TripTimes getNextTrip(State s0, ServiceDay sd, Timetable timetable) { + return timetable.getNextTrip(s0, sd, stopIndex, boarding); + } + + public int calculateWait(State s0, ServiceDay sd, TripTimes tripTimes) { + return boarding ? + (int)(sd.time(tripTimes.getDepartureTime(stopIndex)) - s0.getTimeSeconds()): + (int)(s0.getTimeSeconds() - sd.time(tripTimes.getArrivalTime(stopIndex))); + } + + public boolean isDeviated() { + return false; + } + /** @return the stop where this board/alight edge is located. */ private Stop getStop() { PatternStopVertex stopVertex = (PatternStopVertex) (boarding ? tov : fromv); diff --git a/src/main/java/org/opentripplanner/routing/edgetype/TripPattern.java b/src/main/java/org/opentripplanner/routing/edgetype/TripPattern.java index cfaed974175..46e540499e5 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/TripPattern.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/TripPattern.java @@ -8,11 +8,12 @@ import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.google.common.io.BaseEncoding; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.Route; import org.opentripplanner.model.Stop; import org.opentripplanner.model.StopPattern; +import org.opentripplanner.model.StopPatternFlexFields; import org.opentripplanner.model.Trip; import org.opentripplanner.api.resource.CoordinateArrayListSequence; import org.opentripplanner.common.MavenVersion; @@ -23,6 +24,7 @@ import org.opentripplanner.routing.core.ServiceDay; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.TraverseMode; +import org.opentripplanner.routing.edgetype.flex.FlexPatternHop; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.trippattern.FrequencyEntry; @@ -31,8 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlTransient; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; @@ -140,7 +140,7 @@ public class TripPattern implements Cloneable, Serializable { /** Holds stop-specific information such as wheelchair accessibility and pickup/dropoff roles. */ // TODO: is this necessary? Can we just look at the Stop and StopPattern objects directly? - @XmlElement int[] perStopFlags; + int[] perStopFlags; /** * A set of serviceIds with at least one trip in this pattern. @@ -217,7 +217,6 @@ public Trip getTrip(int tripIndex) { return trips.get(tripIndex); } - @XmlTransient public List getTrips() { return trips; } @@ -508,7 +507,18 @@ public void makePatternVerticesAndEdges(Graph graph, Map geometriesByShapeId = new HashMap(); private Map distancesByShapeId = new HashMap(); - + + private Map flexAreasById = new HashMap<>(); + private FareServiceFactory fareServiceFactory; private Multimap tripPatterns = HashMultimap.create(); @@ -298,6 +302,9 @@ public void run(Graph graph) { // Perhaps it is to allow name collisions with previously loaded feeds. clearCachedData(); + loadFlexAreaMap(); + loadFlexAreasIntoGraph(graph); + /* Assign 0-based numeric codes to all GTFS service IDs. */ for (FeedScopedId serviceId : transitService.getAllServiceIds()) { // TODO: FIX Service code collision for multiple feeds. @@ -368,8 +375,13 @@ public void run(Graph graph) { directionId = -1; } + boolean hasFlexService = stopTimes.stream().anyMatch(this::stopTimeHasFlex); + /* Get the existing TripPattern for this filtered StopPattern, or create one. */ - StopPattern stopPattern = new StopPattern(stopTimes); + StopPattern stopPattern = new StopPattern(stopTimes, graph.deduplicator); + if (hasFlexService) { + stopPattern.setFlexFields(new StopPatternFlexFields(stopTimes, flexAreasById, graph.deduplicator)); + } TripPattern tripPattern = findOrCreateTripPattern(stopPattern, trip.getRoute(), directionId); /* Create a TripTimes object for this list of stoptimes, which form one trip. */ @@ -1015,6 +1027,7 @@ private void clearCachedData() { geometriesByShapeId.clear(); distancesByShapeId.clear(); geometriesByShapeSegmentKey.clear(); + flexAreasById.clear(); } private void loadTransfers(Graph graph) { @@ -1292,10 +1305,18 @@ private TIntList removeRepeatedStops (List stopTimes) { StopTime prev = null; Iterator it = stopTimes.iterator(); TIntList stopSequencesRemoved = new TIntArrayList(); + double serviceAreaRadius = 0.0d; + String serviceAreaWkt = null; while (it.hasNext()) { StopTime st = it.next(); + if (st.getStartServiceAreaRadius() != StopTime.MISSING_VALUE) { + serviceAreaRadius = st.getStartServiceAreaRadius(); + } + if (st.getStartServiceArea() != null) { + serviceAreaWkt = st.getStartServiceArea().getWkt(); + } if (prev != null) { - if (prev.getStop().equals(st.getStop())) { + if (prev.getStop().equals(st.getStop()) && serviceAreaRadius == 0.0d && serviceAreaWkt == null) { // OBA gives us unmodifiable lists, but we have copied them. // Merge the two stop times, making sure we're not throwing out a stop time with times in favor of an @@ -1313,6 +1334,12 @@ private TIntList removeRepeatedStops (List stopTimes) { } } prev = st; + if (st.getEndServiceAreaRadius() != StopTime.MISSING_VALUE) { + serviceAreaRadius = 0.0d; + } + if (st.getEndServiceArea() != null) { + serviceAreaWkt = null; + } } return stopSequencesRemoved; } @@ -1438,12 +1465,10 @@ public void setStopContext(GtfsStopContext context) { this.context = context; } - public double getMaxStopToShapeSnapDistance() { return maxStopToShapeSnapDistance; } - public void setMaxStopToShapeSnapDistance(double maxStopToShapeSnapDistance) { this.maxStopToShapeSnapDistance = maxStopToShapeSnapDistance; } @@ -1500,4 +1525,25 @@ private Collection expandTransfer (Transfer source) { return expandedTransfers; } } + private void loadFlexAreaMap() { + for (FlexArea flexArea : transitService.getAllAreas()) { + Geometry geometry = GeometryUtils.parseWkt(flexArea.getWkt()); + flexAreasById.put(flexArea.getAreaId(), geometry); + } + } + + private void loadFlexAreasIntoGraph(Graph graph) { + for (Map.Entry entry : flexAreasById.entrySet()) { + FeedScopedId id = new FeedScopedId(feedId.getId(), entry.getKey()); + graph.flexAreasById.put(id, entry.getValue()); + } + } + + private boolean stopTimeHasFlex(StopTime st) { + return (st.getContinuousPickup() != 1 && st.getContinuousPickup() != StopTime.MISSING_VALUE) + || (st.getContinuousDropOff() != 1 && st.getContinuousDropOff() != StopTime.MISSING_VALUE) + || st.getStartServiceArea() != null || st.getEndServiceArea() != null + || st.getStartServiceAreaRadius() != StopTime.MISSING_VALUE + || st.getEndServiceAreaRadius() != StopTime.MISSING_VALUE; + } } diff --git a/src/main/java/org/opentripplanner/routing/edgetype/factory/TransferGraphLinker.java b/src/main/java/org/opentripplanner/routing/edgetype/factory/TransferGraphLinker.java index ba10f066982..a4f49ab3282 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/factory/TransferGraphLinker.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/factory/TransferGraphLinker.java @@ -13,8 +13,8 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.vertextype.TransitStationStop; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; /** Link graph based on transfers.txt. Intended for testing */ @Deprecated diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/FlexPatternHop.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/FlexPatternHop.java new file mode 100644 index 00000000000..7f1b8210263 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/FlexPatternHop.java @@ -0,0 +1,122 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.edgetype.PatternHop; +import org.opentripplanner.routing.vertextype.PatternStopVertex; + +/** A PatternHop with GTFS-Flex service enabled. */ +public class FlexPatternHop extends PatternHop { + + private static final long serialVersionUID = 1L; + + private RequestStops requestPickup = RequestStops.NO; + + private RequestStops requestDropoff = RequestStops.NO; + + private double serviceAreaRadius = 0d; + + private Geometry serviceArea = null; + + public FlexPatternHop(PatternStopVertex from, PatternStopVertex to, Stop begin, Stop end, int stopIndex) { + super(from, to, begin, end, stopIndex, true); + } + + protected FlexPatternHop(PatternStopVertex from, PatternStopVertex to, Stop begin, Stop end, int stopIndex, boolean setInPattern) { + super(from, to, begin, end, stopIndex, setInPattern); + } + + /** + * Return the permissions associated with unscheduled pickups in between the endpoints of this + * PatternHop. This relates to flag-stops in the GTFS-Flex specification; if flex and/or flag + * stops are not enabled, this will always be RequestStops.NO. + */ + public RequestStops getRequestPickup() { + return requestPickup; + } + + /** + * Return the permissions associated with unscheduled dropoffs in between the endpoints of this + * PatternHop. This relates to flag-stops in the GTFS-Flex specification; if flex and/or flag + * stops are not enabled, this will always be RequestStops.NO. + */ + public RequestStops getRequestDropoff() { + return requestDropoff; + } + + /** + * Return whether flag stops are enabled in this hop. Flag stops are enabled if either pickups + * or dropoffs at unscheduled locations can be requested. This is a GTFS-Flex feature. + */ + private boolean hasFlagStopService() { + return requestPickup.allowed() || requestDropoff.allowed(); + } + + @Override + public boolean hasFlexService() { + return hasFlagStopService() || getServiceAreaRadius() > 0 || getServiceArea() != null; + } + + public boolean canRequestService(boolean boarding) { + return boarding ? requestPickup.allowed() : requestDropoff.allowed(); + } + + public double getServiceAreaRadius() { + return serviceAreaRadius; + } + + public Geometry getServiceArea() { + return serviceArea; + } + + public boolean hasServiceArea() { + return serviceArea != null; + } + + public void setRequestPickup(RequestStops requestPickup) { + this.requestPickup = requestPickup; + } + + public void setRequestPickup(int code) { + setRequestPickup(RequestStops.fromGtfs(code)); + } + + public void setRequestDropoff(RequestStops requestDropoff) { + this.requestDropoff = requestDropoff; + } + + public void setRequestDropoff(int code) { + setRequestDropoff(RequestStops.fromGtfs(code)); + } + + public void setServiceAreaRadius(double serviceAreaRadius) { + this.serviceAreaRadius = serviceAreaRadius; + } + + public void setServiceArea(Geometry serviceArea) { + this.serviceArea = serviceArea; + } + + private enum RequestStops { + NO(1), YES(0), PHONE(2), COORDINATE_WITH_DRIVER(3); + + final int gtfsCode; + + RequestStops(int gtfsCode) { + this.gtfsCode = gtfsCode; + } + + private static RequestStops fromGtfs(int code) { + for (RequestStops it : values()) { + if(it.gtfsCode == code) { + return it; + } + } + return NO; + } + + boolean allowed() { + return this != NO; + } + } +} diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/FlexTransitBoardAlight.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/FlexTransitBoardAlight.java new file mode 100644 index 00000000000..5e35bc7be84 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/FlexTransitBoardAlight.java @@ -0,0 +1,127 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.ServiceDay; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.edgetype.TemporaryEdge; +import org.opentripplanner.routing.edgetype.Timetable; +import org.opentripplanner.routing.edgetype.TransitBoardAlight; +import org.opentripplanner.routing.trippattern.TripTimes; +import org.opentripplanner.routing.vertextype.PatternStopVertex; +import org.opentripplanner.routing.vertextype.TransitStopArrive; +import org.opentripplanner.routing.vertextype.TransitStopDepart; +import org.opentripplanner.routing.vertextype.flex.TemporaryTransitStop; + +public class FlexTransitBoardAlight extends TransitBoardAlight implements TemporaryEdge { + + // normalized to [0, 1] + private double startIndex; + private double endIndex; + private PartialPatternHop hop; + + public FlexTransitBoardAlight(TransitStopDepart fromStopVertex, PatternStopVertex toPatternVertex, + int stopIndex, PartialPatternHop hop) { + super(fromStopVertex, toPatternVertex, stopIndex, hop.getMode()); + setIndices(hop); + } + + public FlexTransitBoardAlight(PatternStopVertex fromPatternStop, TransitStopArrive toStationVertex, + int stopIndex, PartialPatternHop hop) { + super(fromPatternStop, toStationVertex, stopIndex, hop.getMode()); + setIndices(hop); + } + + private void setIndices(PartialPatternHop hop) { + if (hop.getOriginalHopLength() > 0) { + this.startIndex = hop.getStartIndex() / hop.getOriginalHopLength(); + this.endIndex = hop.getEndIndex() / hop.getOriginalHopLength(); + } else { + // entirely-deviated area hop. Never add entire path time. + this.startIndex = 0.0d; + this.endIndex = 0.0d; + } + this.hop = hop; + } + + @Override + public State traverse(State s0) { + // do not board call-n-ride if it is not a temporary stop and we aren't doing a fixed route-C&R transfer + if (!s0.getOptions().arriveBy && boarding && hop.isDeviatedRouteBoard() + && !((TransitStopDepart) getFromVertex()).getStopVertex().checkCallAndRideBoardAlightOk(s0)) { + return null; + } + + if (s0.getOptions().arriveBy && !boarding && hop.isDeviatedRouteAlight() + && !(((TransitStopArrive) getToVertex()).getStopVertex().checkCallAndRideBoardAlightOk(s0))) { + return null; + } + + return super.traverse(s0); + } + + @Override + public TripTimes getNextTrip(State s0, ServiceDay sd, Timetable timetable) { + if (hop.isUnscheduled()) { + RoutingRequest options = s0.getOptions(); + int time = (int) Math.round(hop.timeLowerBound(options)); + return timetable.getNextCallNRideTrip(s0, sd, getStopIndex(), boarding, time); + } + double adjustment = boarding ? startIndex : -1 * (1 - endIndex); + return timetable.getNextTrip(s0, sd, getStopIndex(), boarding, adjustment, hop.getStartVehicleTime(), hop.getEndVehicleTime()); + } + + @Override + public int calculateWait(State s0, ServiceDay sd, TripTimes tripTimes) { + if (hop.isUnscheduled()) { + int currTime = sd.secondsSinceMidnight(s0.getTimeSeconds()); + boolean useClockTime = !s0.getOptions().flexIgnoreDrtAdvanceBookMin; + long clockTime = s0.getOptions().clockTimeSec; + if (boarding) { + int scheduledTime = tripTimes.getCallAndRideBoardTime(getStopIndex(), currTime, (int) hop.timeLowerBound(s0.getOptions()), sd, useClockTime, clockTime); + if (scheduledTime < 0) + throw new IllegalArgumentException("Unexpected bad wait time"); + return (int) (sd.time(scheduledTime) - s0.getTimeSeconds()); + } else { + int scheduledTime = tripTimes.getCallAndRideAlightTime(getStopIndex(), currTime, (int) hop.timeLowerBound(s0.getOptions()), sd, useClockTime, clockTime); + if (scheduledTime < 0) + throw new IllegalArgumentException("Unexpected bad wait time"); + return (int) (s0.getTimeSeconds() - (sd.time(scheduledTime))); + } + } + int stopIndex = getStopIndex(); + if (boarding) { + int startVehicleTime = hop.getStartVehicleTime(); + if (startVehicleTime != 0) { + startVehicleTime = tripTimes.getDemandResponseMaxTime(startVehicleTime); + } + int offset = (int) Math.round(startIndex * (tripTimes.getRunningTime(stopIndex))); + return (int)(sd.time(tripTimes.getDepartureTime(stopIndex) + offset - startVehicleTime) - s0.getTimeSeconds()); + } + else { + int endVehicleTime = hop.getEndVehicleTime(); + if (endVehicleTime != 0) { + endVehicleTime = tripTimes.getDemandResponseMaxTime(endVehicleTime); + } + int offset = (int) Math.round((1-endIndex) * (tripTimes.getRunningTime(stopIndex - 1))); + return (int)(s0.getTimeSeconds() - sd.time(tripTimes.getArrivalTime(stopIndex) - offset + endVehicleTime)); + } + } + + @Override + public long getExtraWeight(RoutingRequest options) { + boolean deviatedRoute = (boarding && hop.isDeviatedRouteBoard()) || (!boarding && hop.isDeviatedRouteAlight()); + return (deviatedRoute ? options.flexDeviatedRouteExtraPenalty : options.flexFlagStopExtraPenalty); + } + + @Override + public boolean isDeviated() { + return boarding ? hop.isDeviatedRouteBoard() : hop.isDeviatedRouteAlight(); + } + + @Override + public String toString() { + return "FlexTransitBoardAlight(" + + (boarding ? "boarding " : "alighting ") + + getFromVertex() + " to " + getToVertex() + ")"; + } +} diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/PartialPatternHop.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/PartialPatternHop.java new file mode 100644 index 00000000000..abd2ab9f9e6 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/PartialPatternHop.java @@ -0,0 +1,293 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.linearref.LengthIndexedLine; +import org.opentripplanner.api.resource.CoordinateArrayListSequence; +import org.opentripplanner.common.geometry.GeometryUtils; +import org.opentripplanner.common.geometry.SphericalDistanceLibrary; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.edgetype.PatternHop; +import org.opentripplanner.routing.trippattern.TripTimes; +import org.opentripplanner.routing.vertextype.PatternArriveVertex; +import org.opentripplanner.routing.vertextype.PatternDepartVertex; +import org.opentripplanner.routing.vertextype.PatternStopVertex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PartialPatternHop extends FlexPatternHop { + + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(PartialPatternHop.class); + + private double startIndex; + private double endIndex; + private double originalHopLength; + private double percentageOfHop; + private FlexPatternHop originalHop; + private Geometry boardArea; + private Geometry alightArea; + private LineString displayGeometry; + + // if we have this, it's a deviated-route hop + // these are "direct" times, ie drive times without DRT service parameteers applied + private int startVehicleTime = 0; + private int endVehicleTime = 0; + private LineString startGeometry; + private LineString endGeometry; + + // constructor for flag stops + // this could be merged into deviated-route service constructor + public PartialPatternHop(FlexPatternHop hop, PatternStopVertex from, PatternStopVertex to, Stop fromStop, Stop toStop, double startIndex, double endIndex, double buffer) { + super(from, to, fromStop, toStop, hop.getStopIndex(), false); + setRequestPickup(hop.getRequestPickup()); + setRequestDropoff(hop.getRequestDropoff()); + setServiceAreaRadius(hop.getServiceAreaRadius()); + setServiceArea(hop.getServiceArea()); + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + this.startIndex = startIndex; + this.endIndex = endIndex; + this.percentageOfHop = (this.endIndex - this.startIndex) / line.getEndIndex(); + this.originalHop = hop; + this.originalHopLength = line.getEndIndex(); + setGeometry(hop, line, buffer, buffer); + } + + // constructor for deviated-route service + public PartialPatternHop(FlexPatternHop hop, PatternStopVertex from, PatternStopVertex to, Stop fromStop, Stop toStop, double startIndex, double endIndex, + LineString startGeometry, int startVehicleTime, LineString endGeometry, int endVehicleTime, double buffer) { + super(from, to, fromStop, toStop, hop.getStopIndex(), false); + setRequestPickup(hop.getRequestPickup()); + setRequestDropoff(hop.getRequestDropoff()); + setServiceAreaRadius(hop.getServiceAreaRadius()); + setServiceArea(hop.getServiceArea()); + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + this.startIndex = startIndex; + this.endIndex = endIndex; + this.percentageOfHop = (this.endIndex - this.startIndex) / line.getEndIndex(); + this.originalHop = hop; + this.originalHopLength = line.getEndIndex(); + this.startVehicleTime = startVehicleTime; + this.endVehicleTime = endVehicleTime; + this.startGeometry = startGeometry; + this.endGeometry = endGeometry; + + // Only area on the route will be part of display geometry. + boolean flagStopBoard = startIndex > 0 && startVehicleTime == 0; + boolean flagStopAlight = endIndex < line.getEndIndex() && endVehicleTime == 0; + if (!hop.getBeginStop().equals(hop.getEndStop())) { + setGeometry(hop, line, flagStopBoard ? buffer : 0, flagStopAlight ? buffer : 0); + } else { + // If this hop has no geometry (entirely flexible deviated-route): + Coordinate c = hop.getFromVertex().getCoordinate(); + displayGeometry = GeometryUtils.getGeometryFactory().createLineString(new Coordinate[]{c, c}); + percentageOfHop = 0; + } + + LineString transitGeometry = (LineString) line.extractLine(startIndex, endIndex); + CoordinateArrayListSequence coordinates = new CoordinateArrayListSequence(); + if (startGeometry != null) { + coordinates.extend(startGeometry.getCoordinates()); + } + coordinates.extend(transitGeometry.getCoordinates()); + if (endGeometry != null) { + coordinates.extend(endGeometry.getCoordinates()); + } + LineString geometry = GeometryUtils.getGeometryFactory().createLineString(coordinates); + setGeometry(geometry); + } + + // pass-thru for TemporaryDirectPatternHop + public PartialPatternHop(FlexPatternHop hop, PatternStopVertex from, PatternStopVertex to, Stop fromStop, Stop toStop) { + super(from, to, fromStop, toStop, hop.getStopIndex(), false); + setRequestPickup(hop.getRequestPickup()); + setRequestDropoff(hop.getRequestDropoff()); + setServiceAreaRadius(hop.getServiceAreaRadius()); + setServiceArea(hop.getServiceArea()); + this.originalHop = hop; + } + + private void setGeometry(PatternHop hop, LengthIndexedLine line, double boardBuffer, double alightBuffer) { + double pointsPerMeter = (line.getEndIndex() - line.getStartIndex()) / SphericalDistanceLibrary.fastLength(hop.getGeometry()); + double boardBufferPts = boardBuffer * pointsPerMeter; + double alightBufferPts = alightBuffer * pointsPerMeter; + double start = Math.max(line.getStartIndex(), startIndex - boardBufferPts); + double end = Math.min(line.getEndIndex(), endIndex + alightBufferPts); + displayGeometry = (LineString) line.extractLine(start, end); + Geometry geom = line.extractLine(startIndex, endIndex); + if (geom instanceof LineString) { // according to the javadocs, it is. + setGeometry((LineString) geom); + } + if (startIndex > line.getStartIndex() && boardBuffer > 0) { + boardArea = line.extractLine(start, Math.min(startIndex + boardBufferPts, end)); + } + if (endIndex < line.getEndIndex() && alightBuffer > 0) { + alightArea = line.extractLine(Math.max(endIndex - alightBufferPts, start), end); + } + } + + // given hop s0->s1 and a temporary position t, create a partial hop s0->t + public static PartialPatternHop startHop(FlexPatternHop hop, PatternArriveVertex to, Stop toStop) { + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + return new PartialPatternHop(hop, (PatternStopVertex) hop.getFromVertex(), to, hop.getBeginStop(), toStop, line.getStartIndex(), line.project(to.getCoordinate()), 0); + } + + public static PartialPatternHop endHop(FlexPatternHop hop, PatternDepartVertex from, Stop fromStop) { + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + return new PartialPatternHop(hop, from, (PatternStopVertex) hop.getToVertex(), fromStop, hop.getEndStop(), line.project(from.getCoordinate()), line.getEndIndex(), 0); + } + + @Override + public double timeLowerBound(RoutingRequest options) { + return Math.floor(percentageOfHop * super.timeLowerBound(options)) + startVehicleTime + endVehicleTime; + } + + @Override + public int getRunningTime(State s0) { + TripTimes tt = s0.getTripTimes(); + int vehicleTime = startVehicleTime + endVehicleTime; + if (vehicleTime > 0) { + vehicleTime = tt.getDemandResponseMaxTime(vehicleTime); + } + if (originalHopLength == 0) { + return vehicleTime; + } + double startPct = startIndex / originalHopLength; + double endPct = endIndex / originalHopLength; + // necessary so rounding happens using the same coefficients as in FlexTransitBoardAlight + int arr = tt.getArrivalTime(stopIndex + 1) - (int) Math.round((1 - endPct) * (tt.getRunningTime(stopIndex))); + int dep = tt.getDepartureTime(stopIndex) + (int) Math.round(startPct * (tt.getRunningTime(stopIndex))); + return (arr - dep) + vehicleTime; + } + + @Override + public LineString getDisplayGeometry() { + if (displayGeometry != null) { + return displayGeometry; + } + return getGeometry(); + } + + // is this hop too not-different to care about? for now lets say should be > 50 m shorter than original hop + public boolean isTrivial(RoutingRequest options) { + if ((isDeviatedRouteBoard() && getStartVehicleTime() < 5) || (isDeviatedRouteAlight() && getEndVehicleTime() < 5)) + return true; + double length = SphericalDistanceLibrary.fastLength(getGeometry()); + double parentLength = SphericalDistanceLibrary.fastLength(getOriginalHop().getGeometry()); + if (length == 0 || length < options.flexMinPartialHopLength) { + return true; + } + if (parentLength == 0) { + return length < 5d; // deviated route + } + // Test for bad transit edges. + double fromDist = SphericalDistanceLibrary.distance(getFromVertex().getCoordinate(), + getGeometry().getStartPoint().getCoordinate()); + double toDist = SphericalDistanceLibrary.distance(getToVertex().getCoordinate(), + getGeometry().getEndPoint().getCoordinate()); + if (fromDist > 400.0 || toDist > 400.0) { + LOG.info("Discarding edge: mismatch between endpoints and street geometry. This " + + "indicates bad transit stop linking at {} or {}", + getOriginalHop().getBeginStop(), getOriginalHop().getEndStop()); + return true; + } + return length + 50 >= parentLength; + } + + /** + * Return true if "unscheduled" ie call-n-ride + */ + public boolean isUnscheduled() { + return false; + } + + public boolean isDeviatedRouteService() { + return startVehicleTime > 0 || endVehicleTime > 0; + } + + public boolean isDeviatedRouteBoard() { + return startVehicleTime > 0; + } + + public boolean isDeviatedRouteAlight() { + return endVehicleTime > 0; + } + + public boolean isFlagStopBoard() { + return startIndex > 0 && startVehicleTime == 0; + } + + public boolean isFlagStopAlight() { + return endIndex < originalHopLength && endVehicleTime == 0; + } + + public boolean isOriginalHop(PatternHop hop) { + return originalHop.getId() == hop.getId(); + } + + public boolean hasBoardArea() { + return boardArea != null; + } + + public boolean hasAlightArea() { + return alightArea != null; + } + + public FlexPatternHop getOriginalHop() { + return originalHop; + } + + public double getPercentageOfHop() { + return percentageOfHop; + } + + public double getStartIndex() { + return startIndex; + } + + public double getEndIndex() { + return endIndex; + } + + public double getOriginalHopLength() { + return originalHopLength; + } + + public Geometry getBoardArea() { + return boardArea; + } + + public Geometry getAlightArea() { + return alightArea; + } + + public int getStartVehicleTime() { + return startVehicleTime; + } + + public int getEndVehicleTime() { + return endVehicleTime; + } + + public LineString getStartGeometry() { + return startGeometry; + } + + public LineString getEndGeometry() { + return endGeometry; + } + + public int getDirectVehicleTime() { + return startVehicleTime + endVehicleTime; + } + + @Override + public String getFeedId() { + return originalHop.getFeedId(); + } +} + diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryDirectPatternHop.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryDirectPatternHop.java new file mode 100644 index 00000000000..fa4fe2526d6 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryDirectPatternHop.java @@ -0,0 +1,81 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.core.StateEditor; +import org.opentripplanner.routing.edgetype.TemporaryEdge; +import org.opentripplanner.routing.trippattern.TripTimes; +import org.opentripplanner.routing.vertextype.PatternStopVertex; + +/** + * This is associated with a PatternHop for stop_time information, but its geometry bears no + * relation to the route geometry. And its timing is approximate. + */ +public class TemporaryDirectPatternHop extends TemporaryPartialPatternHop implements TemporaryEdge { + private static final long serialVersionUID = 1L; + + /* + * This is the direct time a car would take to do this hop. Based on DRT service parameters, + * it actually may take a different amount of time. + */ + private int directTime; + + public TemporaryDirectPatternHop(FlexPatternHop hop, PatternStopVertex from, PatternStopVertex to, Stop fromStop, Stop toStop, LineString geometry, int time) { + super(hop, from, to, fromStop, toStop); + setGeometry(geometry); + this.directTime = time; + } + + @Override + public boolean isUnscheduled() { + return true; + } + + @Override + public boolean isTrivial(RoutingRequest options) { + return false; + } + + @Override + public double timeLowerBound(RoutingRequest options) { + return directTime; + } + + @Override + public int getRunningTime(State s0) { + TripTimes tt = s0.getTripTimes(); + return tt.getDemandResponseMaxTime(directTime); + } + + @Override + public int getWeight(State s0, int runningTime) { + return (int) Math.round(s0.getOptions().flexCallAndRideReluctance * runningTime); + } + + @Override + public boolean isDeviatedRouteBoard() { + return true; + } + + @Override + public boolean isDeviatedRouteAlight() { + return true; + } + + @Override + public int getDirectVehicleTime() { + return directTime; + } + + @Override + public State traverse(State s0) { + StateEditor s1 = s0.edit(this); + s1.incrementCallAndRideTime(directTime); + if (s1.getCallAndRideTime() >= s0.getOptions().flexMaxCallAndRideSeconds) { + return null; + } + return super.traverse(s0, s1); + } +} diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPartialPatternHop.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPartialPatternHop.java new file mode 100644 index 00000000000..9b0fbe53326 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPartialPatternHop.java @@ -0,0 +1,22 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.edgetype.TemporaryEdge; +import org.opentripplanner.routing.vertextype.PatternStopVertex; + +public class TemporaryPartialPatternHop extends PartialPatternHop implements TemporaryEdge { + public TemporaryPartialPatternHop(FlexPatternHop hop, PatternStopVertex from, PatternStopVertex to, Stop fromStop, Stop toStop, double startIndex, double endIndex, double buffer) { + super(hop, from, to, fromStop, toStop, startIndex, endIndex, buffer); + } + + public TemporaryPartialPatternHop(FlexPatternHop hop, PatternStopVertex from, PatternStopVertex to, Stop fromStop, Stop toStop, double startIndex, double endIndex, + LineString startGeometry, int startVehicleTime, LineString endGeometry, int endVehicleTime, double buffer) { + super(hop, from, to, fromStop, toStop, startIndex, endIndex, startGeometry, startVehicleTime, endGeometry, endVehicleTime, buffer); + } + + // pass-thru for TemporaryDirectPatternHop + public TemporaryPartialPatternHop(FlexPatternHop hop, PatternStopVertex from, PatternStopVertex to, Stop fromStop, Stop toStop) { + super(hop, from, to, fromStop, toStop); + } +} diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPreAlightEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPreAlightEdge.java new file mode 100644 index 00000000000..e1ade75b093 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPreAlightEdge.java @@ -0,0 +1,16 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.opentripplanner.routing.edgetype.PreAlightEdge; +import org.opentripplanner.routing.edgetype.StationEdge; +import org.opentripplanner.routing.edgetype.TemporaryEdge; +import org.opentripplanner.routing.vertextype.TransitStop; +import org.opentripplanner.routing.vertextype.TransitStopArrive; + + +public class TemporaryPreAlightEdge extends PreAlightEdge implements StationEdge, TemporaryEdge { + + public TemporaryPreAlightEdge(TransitStopArrive from, TransitStop to) { + super(from, to); + } + +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPreBoardEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPreBoardEdge.java new file mode 100644 index 00000000000..7065246fec2 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryPreBoardEdge.java @@ -0,0 +1,24 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.opentripplanner.routing.edgetype.PreBoardEdge; +import org.opentripplanner.routing.edgetype.StationEdge; +import org.opentripplanner.routing.edgetype.TemporaryEdge; +import org.opentripplanner.routing.vertextype.TransitStop; +import org.opentripplanner.routing.vertextype.TransitStopDepart; + +/** + * PreBoard edges connect a TransitStop to its agency_stop_depart vertices; PreAlight edges connect + * an agency_stop_arrive vertex to its TransitStop. + * + * Applies the local stop rules (see TransitStop.java and LocalStopFinder.java) as well as transfer + * limits, timed and preferred transfer rules, transfer penalties, and boarding costs. This avoids + * applying these costs/rules repeatedly in (Pattern)Board edges. These are single station or + * station-to-station specific costs, rather than trip-pattern specific costs. + */ +public class TemporaryPreBoardEdge extends PreBoardEdge implements StationEdge, TemporaryEdge { + + public TemporaryPreBoardEdge(TransitStop from, TransitStopDepart to) { + super(from, to); + } + +} diff --git a/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryStreetTransitLink.java b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryStreetTransitLink.java new file mode 100644 index 00000000000..0887f01d6ec --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/edgetype/flex/TemporaryStreetTransitLink.java @@ -0,0 +1,24 @@ +package org.opentripplanner.routing.edgetype.flex; + +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.edgetype.StreetTransitLink; +import org.opentripplanner.routing.edgetype.TemporaryEdge; +import org.opentripplanner.routing.vertextype.StreetVertex; +import org.opentripplanner.routing.vertextype.TransitStop; + +public class TemporaryStreetTransitLink extends StreetTransitLink implements TemporaryEdge { + + + public TemporaryStreetTransitLink(StreetVertex fromv, TransitStop tov, boolean wheelchairAccessible) { + super(fromv, tov, wheelchairAccessible); + } + + public TemporaryStreetTransitLink(TransitStop fromv, StreetVertex tov, boolean wheelchairAccessible) { + super(fromv, tov, wheelchairAccessible); + } + + public State traverse(State s0) { + return super.traverse(s0); + } + +} diff --git a/src/main/java/org/opentripplanner/routing/flex/CarPermissionSearch.java b/src/main/java/org/opentripplanner/routing/flex/CarPermissionSearch.java new file mode 100644 index 00000000000..1c623dbdec3 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/flex/CarPermissionSearch.java @@ -0,0 +1,84 @@ +package org.opentripplanner.routing.flex; + +import org.opentripplanner.routing.algorithm.GenericDijkstra; +import org.opentripplanner.routing.algorithm.TraverseVisitor; +import org.opentripplanner.routing.algorithm.strategies.SearchTerminationStrategy; +import org.opentripplanner.routing.algorithm.strategies.TrivialRemainingWeightHeuristic; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.core.TraverseMode; +import org.opentripplanner.routing.core.TraverseModeSet; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.edgetype.TemporaryEdge; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Vertex; +import org.opentripplanner.routing.spt.ShortestPathTree; + +public class CarPermissionSearch { + + private RoutingRequest opt; + + public CarPermissionSearch(RoutingRequest opt, boolean arriveBy) { + this.opt = opt.clone(); + this.opt.setMode(TraverseMode.WALK); + this.opt.setArriveBy(arriveBy); + } + + public Vertex findVertexWithPermission(Vertex vertex, TraverseMode mode) { + SearchStrategy strategy = new SearchStrategy(mode); + GenericDijkstra gd = new GenericDijkstra(opt); + gd.setHeuristic(new TrivialRemainingWeightHeuristic()); + gd.traverseVisitor = strategy; + gd.setSearchTerminationStrategy(strategy); + gd.getShortestPathTree(new State(vertex, opt)); + return strategy.getVertex(); + } + + private static class SearchStrategy implements SearchTerminationStrategy, TraverseVisitor { + + private final TraverseMode mode; + + private Vertex vertex = null; + + private boolean firstStreetEdge = true; + + public SearchStrategy(TraverseMode mode) { + this.mode = mode; + } + + // TraverseVisitor + + @Override + public void visitEdge(Edge edge, State state) { + boolean arriveBy = state.getOptions().arriveBy; + if (vertex == null && edge instanceof StreetEdge && !(edge instanceof TemporaryEdge)) { + if (((StreetEdge) edge).canTraverse(new TraverseModeSet(mode))) { + if (firstStreetEdge) { + vertex = arriveBy ? state.getOptions().rctx.toVertex : state.getOptions().rctx.fromVertex; + } else { + vertex = arriveBy ? edge.getToVertex() : edge.getFromVertex(); + } + } + firstStreetEdge = false; + } + } + + @Override + public void visitVertex(State state) { + } + + @Override + public void visitEnqueue(State state) { + } + + // TerminationStrategy + @Override + public boolean shouldSearchTerminate(Vertex origin, Vertex target, State current, ShortestPathTree spt, RoutingRequest traverseOptions) { + return vertex != null; + } + + public Vertex getVertex() { + return vertex; + } + } +} diff --git a/src/main/java/org/opentripplanner/routing/flex/DeviatedRouteGraphModifier.java b/src/main/java/org/opentripplanner/routing/flex/DeviatedRouteGraphModifier.java new file mode 100644 index 00000000000..b2bf98cd57f --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/flex/DeviatedRouteGraphModifier.java @@ -0,0 +1,248 @@ +package org.opentripplanner.routing.flex; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.linearref.LengthIndexedLine; +import org.opentripplanner.common.geometry.GeometryUtils; +import org.opentripplanner.common.geometry.SphericalDistanceLibrary; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.algorithm.strategies.SearchTerminationStrategy; +import org.opentripplanner.routing.core.RoutingContext; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.core.TraverseMode; +import org.opentripplanner.routing.edgetype.flex.FlexPatternHop; +import org.opentripplanner.routing.edgetype.TripPattern; +import org.opentripplanner.routing.edgetype.flex.TemporaryPartialPatternHop; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.Vertex; +import org.opentripplanner.routing.spt.GraphPath; +import org.opentripplanner.routing.spt.ShortestPathTree; +import org.opentripplanner.routing.vertextype.PatternArriveVertex; +import org.opentripplanner.routing.vertextype.PatternDepartVertex; +import org.opentripplanner.routing.vertextype.PatternStopVertex; +import org.opentripplanner.routing.vertextype.StreetVertex; +import org.opentripplanner.routing.vertextype.TemporaryVertex; +import org.opentripplanner.routing.vertextype.TransitStop; +import org.opentripplanner.routing.vertextype.flex.TemporaryTransitStop; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Add temporary vertices/edges for deviated-route service. + */ +public class DeviatedRouteGraphModifier extends GtfsFlexGraphModifier { + + // want to ensure we only keep one pattern hop per trip pattern + private Map directServices = Maps.newHashMap(); + private Set transitStopStates = Sets.newHashSet(); + + public DeviatedRouteGraphModifier(Graph graph) { + super(graph); + } + + @Override + public TraverseMode getMode() { + return TraverseMode.CAR; + } + + @Override + public SearchTerminationStrategy getSearchTerminationStrategy() { + return new SearchTerminationStrategy() { + @Override + public boolean shouldSearchTerminate(Vertex origin, Vertex target, State current, ShortestPathTree spt, RoutingRequest traverseOptions) { + return current.getElapsedTimeSeconds() > traverseOptions.flexMaxCallAndRideSeconds; + } + }; + } + + @Override + public TemporaryPartialPatternHop makeHopNewTo(RoutingRequest opt, State state, FlexPatternHop hop, PatternArriveVertex to, Stop toStop) { + GraphPath path = new GraphPath(state, false); + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + double startIndex = line.getStartIndex(); + double endIndex = line.project(state.getBackEdge().getToVertex().getCoordinate()); + if (hop.getStopIndex() == 0 && tooLittleOnRoute(hop, line, startIndex, endIndex)) { + StreetVertex toVertex = findFirstStreetVertex(opt.rctx, true); + TemporaryTransitStop toTempStop = getTemporaryStop(toVertex, null, opt.rctx, opt); + TransitStop fromStop = graph.index.stopVertexForStop.get(hop.getBeginStop()); + createDirectHop(opt, hop, fromStop, toTempStop, path); + return null; + } + return new TemporaryPartialPatternHop(hop, (PatternStopVertex) hop.getFromVertex(), to, hop.getBeginStop(), toStop, + startIndex, endIndex, null, 0, path.getGeometry(), path.getDuration(), opt.flexFlagStopBufferSize); + } + + @Override + public TemporaryPartialPatternHop makeHopNewFrom(RoutingRequest opt, State state, FlexPatternHop hop, PatternDepartVertex from, Stop fromStop) { + GraphPath path = new GraphPath(state, false); + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + // state is place where we meet line + double startIndex = line.project(state.getBackEdge().getFromVertex().getCoordinate()); + double endIndex = line.getEndIndex(); + if (hop.getStopIndex() + 1 == hop.getPattern().getPatternHops().size() && tooLittleOnRoute(hop, line, startIndex, endIndex)) { + StreetVertex fromVertex = findFirstStreetVertex(opt.rctx, false); + TemporaryTransitStop fromTempStop = getTemporaryStop(fromVertex, null, opt.rctx, opt); + TransitStop toStop = graph.index.stopVertexForStop.get(hop.getEndStop()); + createDirectHop(opt, hop, fromTempStop, toStop, path); + return null; + } + return new TemporaryPartialPatternHop(hop, from, (PatternStopVertex) hop.getToVertex(), fromStop, hop.getEndStop(), + startIndex, endIndex, path.getGeometry(), path.getDuration(), null, 0, opt.flexFlagStopBufferSize); + } + + @Override + public TemporaryPartialPatternHop shortenEnd(RoutingRequest opt, State state, TemporaryPartialPatternHop hop, PatternStopVertex to, Stop toStop) { + FlexPatternHop originalHop = hop.getOriginalHop(); + GraphPath path = new GraphPath(state, false); + LengthIndexedLine line = new LengthIndexedLine(originalHop.getGeometry()); + double startIndex = hop.getStartIndex(); + double endIndex = line.project(state.getBackEdge().getToVertex().getCoordinate()); + if (endIndex < startIndex) + return null; + // we may want to create a ~direct~ hop. + // let's say, create a direct hop if the distance we would travel on the route is < 100m. We'll do this in vertexVisitor later. + if (tooLittleOnRoute(originalHop, line, startIndex, endIndex)) { + return null; + } else { + return new TemporaryPartialPatternHop(originalHop, (PatternStopVertex) hop.getFromVertex(), to, hop.getBeginStop(), toStop, + startIndex, endIndex, hop.getStartGeometry(), hop.getStartVehicleTime(), path.getGeometry(), path.getDuration(), opt.flexFlagStopBufferSize); + } + } + + private boolean tooLittleOnRoute(FlexPatternHop originalHop, LengthIndexedLine line, double startIndex, double endIndex) { + double onRouteDistance = SphericalDistanceLibrary.fastLength((LineString) line.extractLine(startIndex, endIndex)); + return onRouteDistance <= Math.min(100, originalHop.getDistance()); + } + + @Override + public boolean checkHopAllowsBoardAlight(State s, FlexPatternHop hop, boolean boarding) { + StreetVertex sv = findFirstStreetVertex(s); + // If first vertex is not a StreetVertex, it's a transit vertex, which we'll catch later. + if (sv == null) { + // Do check for service + if (!s.getOptions().arriveBy) { + Point pt = GeometryUtils.getGeometryFactory().createPoint(s.getOptions().rctx.fromVertex.getCoordinate()); + if (addHopAsDirectService(hop, pt)) { + directServices.put(hop.getPattern(), hop); + } + } + return false; + } + + Point orig = GeometryUtils.getGeometryFactory().createPoint(sv.getCoordinate()); + Point dest = GeometryUtils.getGeometryFactory().createPoint(s.getVertex().getCoordinate()); + boolean ret = false; + if (hop.hasServiceArea()) { + if (hop.getServiceArea().contains(orig) && hop.getServiceArea().contains(dest)) { + ret = true; + } + } + if (!ret && hop.getServiceAreaRadius() > 0) { + double distance = SphericalDistanceLibrary.distance(s.getVertex().getCoordinate(), sv.getCoordinate()); + ret = distance < hop.getServiceAreaRadius(); + } + if (addHopAsDirectService(hop, orig)) { + directServices.put(hop.getPattern(), hop); + } + return ret; + } + + private boolean addHopAsDirectService(FlexPatternHop hop, Point orig) { + return hop.hasServiceArea() && hop.getServiceArea().contains(orig) && directServices.get(hop.getPattern()) == null; + } + + @Override + public void vertexVisitor(State state) { + if (state.getVertex() instanceof TransitStop && !(state.getVertex() instanceof TemporaryVertex)) { + if (((TransitStop) state.getVertex()).getModes().contains(TraverseMode.BUS)) { + transitStopStates.add(state); + } + } + // Direct hop to destination if found + boolean foundTarget = state.getVertex() == state.getOptions().rctx.toVertex; + if (!state.getOptions().arriveBy && foundTarget) { + transitStopStates.add(state); + } + } + + protected void streetSearch(RoutingRequest rr) { + transitStopStates.clear(); + super.streetSearch(rr); + createDirectHopsToStops(rr); + } + + private void createDirectHopsToStops(RoutingRequest opt) { + Collection services = directServices.values(); + for (State state : transitStopStates) { + Vertex v = state.getVertex(); + Point dest = GeometryUtils.getGeometryFactory().createPoint(v.getCoordinate()); + for (FlexPatternHop hop : services) { + if (hop.getServiceArea().contains(dest)) { + TransitStop fromStop, toStop; + if (opt.arriveBy) { + if (opt.rctx.toVertex instanceof TransitStop) { + toStop = (TransitStop) opt.rctx.toVertex; + } else { + StreetVertex toVertex = findFirstStreetVertex(opt.rctx, true); + toStop = getTemporaryStop(toVertex, null, opt.rctx, opt); + } + Point toPt = GeometryUtils.getGeometryFactory().createPoint(toStop.getCoordinate()); + if (!hop.getServiceArea().contains(toPt)) { + continue; + } + if (!(v instanceof TransitStop)) { + throw new RuntimeException("Unexpected error! Only non-transit stop should be destination"); + } else { + fromStop = (TransitStop) v; + } + } else { + if (opt.rctx.fromVertex instanceof TransitStop) { + fromStop = (TransitStop) opt.rctx.fromVertex; + } else { + StreetVertex fromVertex = findFirstStreetVertex(opt.rctx, false); + fromStop = getTemporaryStop(fromVertex, null, opt.rctx, opt); + } + Point fromPt = GeometryUtils.getGeometryFactory().createPoint(fromStop.getCoordinate()); + if (!hop.getServiceArea().contains(fromPt)) { + continue; + } + if (!(v instanceof TransitStop)) { + if (v == state.getOptions().rctx.toVertex) { + StreetVertex toVertex = findFirstStreetVertex(opt.rctx, true); + toStop = getTemporaryStop(toVertex, null, opt.rctx, opt, false); + } else { + throw new RuntimeException("Unexpected error! Only non-transit stop should be destination"); + } + } else { + toStop = (TransitStop) v; + } + } + createDirectHop(opt, hop, fromStop, toStop, new GraphPath(state, true)); + } + } + } + } + + @Override + public StreetVertex getLocationForTemporaryStop(State s, FlexPatternHop hop) { + return findFirstStreetVertex(s); + } + + // Return null if first vertex is not a street vertex + private StreetVertex findFirstStreetVertex(State s) { + return findFirstStreetVertex(s.getOptions().rctx, s.getOptions().arriveBy); + } + + private StreetVertex findFirstStreetVertex(RoutingContext rctx, boolean reverse) { + Vertex v = reverse ? rctx.toVertex : rctx.fromVertex; + if (v instanceof StreetVertex) { + return (StreetVertex) v; + } + return null; + } +} diff --git a/src/main/java/org/opentripplanner/routing/flex/FlagStopGraphModifier.java b/src/main/java/org/opentripplanner/routing/flex/FlagStopGraphModifier.java new file mode 100644 index 00000000000..9010f0bd1ba --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/flex/FlagStopGraphModifier.java @@ -0,0 +1,110 @@ +package org.opentripplanner.routing.flex; + +import org.locationtech.jts.linearref.LengthIndexedLine; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.algorithm.strategies.SearchTerminationStrategy; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.core.TraverseMode; +import org.opentripplanner.routing.edgetype.flex.FlexPatternHop; +import org.opentripplanner.routing.edgetype.StreetTransitLink; +import org.opentripplanner.routing.edgetype.flex.TemporaryPartialPatternHop; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.Vertex; +import org.opentripplanner.routing.vertextype.PatternArriveVertex; +import org.opentripplanner.routing.vertextype.PatternDepartVertex; +import org.opentripplanner.routing.vertextype.PatternStopVertex; +import org.opentripplanner.routing.vertextype.StreetVertex; +import org.opentripplanner.routing.vertextype.TransitStop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Add temporary vertices/edges for flag stops. + */ +public class FlagStopGraphModifier extends GtfsFlexGraphModifier { + + private static final Logger LOG = LoggerFactory.getLogger(FlagStopGraphModifier.class); + + public FlagStopGraphModifier(Graph graph) { + super(graph); + } + + @Override + public TraverseMode getMode() { + return TraverseMode.WALK; + } + + @Override + public SearchTerminationStrategy getSearchTerminationStrategy() { + return (o, t, state, s, opt) -> state.getWalkDistance() > opt.maxWalkDistance; + } + + @Override + public TemporaryPartialPatternHop makeHopNewTo(RoutingRequest opt, State state, FlexPatternHop hop, PatternArriveVertex to, Stop toStop) { + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + return new TemporaryPartialPatternHop(hop, (PatternStopVertex) hop.getFromVertex(), to, hop.getBeginStop(), toStop, line.getStartIndex(), line.project(to.getCoordinate()), opt.flexFlagStopBufferSize); + } + + @Override + public TemporaryPartialPatternHop makeHopNewFrom(RoutingRequest opt, State state, FlexPatternHop hop, PatternDepartVertex from, Stop fromStop) { + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + return new TemporaryPartialPatternHop(hop, from, (PatternStopVertex) hop.getToVertex(), fromStop, hop.getEndStop(), line.project(from.getCoordinate()), line.getEndIndex(), opt.flexFlagStopBufferSize); + } + + @Override + public TemporaryPartialPatternHop shortenEnd(RoutingRequest opt, State state, TemporaryPartialPatternHop hop, PatternStopVertex to, Stop toStop) { + FlexPatternHop originalHop = hop.getOriginalHop(); + LengthIndexedLine line = new LengthIndexedLine(originalHop.getGeometry()); + double endIndex = line.project(to.getCoordinate()); + if (endIndex < hop.getStartIndex()) + return null; + return new TemporaryPartialPatternHop(originalHop, (PatternStopVertex) hop.getFromVertex(), to, hop.getBeginStop(), toStop, hop.getStartIndex(), endIndex, + hop.getStartGeometry(), hop.getStartVehicleTime(), null, 0, opt.flexFlagStopBufferSize); + } + + @Override + public StreetVertex getLocationForTemporaryStop(State s, FlexPatternHop hop) { + RoutingRequest rr = s.getOptions(); + Vertex initVertex = rr.arriveBy ? rr.rctx.toVertex : rr.rctx.fromVertex; + + Vertex v; + if(s.getVertex() == initVertex){ + //the origin/destination lies along a flag stop route + LOG.debug("the origin/destination lies along a flag stop route."); + v = initVertex; + } else { + v = rr.arriveBy ? s.getBackEdge().getToVertex() : s.getBackEdge().getFromVertex(); + } + + // Ensure on line + LengthIndexedLine line = new LengthIndexedLine(hop.getGeometry()); + double i = line.project(v.getCoordinate()); + if (i <= line.getStartIndex() || i >= line.getEndIndex()) { + return null; + } + + if (v instanceof StreetVertex) { + return (StreetVertex) v; + } else if (v instanceof TransitStop) { + TransitStop tstop = (TransitStop) v; + if (rr.rctx.graph.index.patternsForStop.get(tstop.getStop()).contains(hop.getPattern())) { + LOG.debug("ignoring flag stop at existing stop"); + return null; + } + for (Edge e : tstop.getOutgoing()) { + if (e instanceof StreetTransitLink) { + return (StreetVertex) e.getToVertex(); + } + } + return null; + } + throw new RuntimeException("Unexpected location."); + } + + @Override + public boolean checkHopAllowsBoardAlight(State state, FlexPatternHop hop, boolean boarding) { + return hop.canRequestService(boarding); + } +} diff --git a/src/main/java/org/opentripplanner/routing/flex/FlexIndex.java b/src/main/java/org/opentripplanner/routing/flex/FlexIndex.java new file mode 100644 index 00000000000..4155b941a24 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/flex/FlexIndex.java @@ -0,0 +1,143 @@ +package org.opentripplanner.routing.flex; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.common.geometry.SphericalDistanceLibrary; +import org.opentripplanner.graph_builder.module.map.StreetMatcher; +import org.opentripplanner.routing.edgetype.flex.FlexPatternHop; +import org.opentripplanner.routing.edgetype.PatternHop; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.edgetype.TemporaryPartialStreetEdge; +import org.opentripplanner.routing.edgetype.TripPattern; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Graph; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * This class contains indices needed for flex service. Right now, that's a map between PatternHops + * the StreetEdges which the hop is incident with. There may be more indices if new types of + * flex transit are added. + */ +public class FlexIndex { + + private static final Logger LOG = LoggerFactory.getLogger(FlexIndex.class); + + private final Multimap hopsForEdge = HashMultimap.create(); + + public void init(Graph graph) { + LOG.info("initializing hops-for-edge map..."); + initializeHopsForEdgeMap(graph); + } + + public Collection getHopsForEdge(Edge e) { + if (e instanceof TemporaryPartialStreetEdge) { + e = ((TemporaryPartialStreetEdge) e).getParentEdge(); + } + return hopsForEdge.get(e); + } + + private void initializeHopsForEdgeMap(Graph graph) { + if (!graph.hasStreets) { + LOG.info("Cannot initialize hop-to-street-edge map; graph does not have streets loaded."); + return; + } + StreetMatcher matcher = new StreetMatcher(graph); + LOG.info("Finding corresponding street edges for trip patterns..."); + for (TripPattern pattern : graph.index.patternForId.values()) { + if (pattern.hasFlexService()) { + LOG.debug("Matching {}", pattern); + for(PatternHop ph : pattern.getPatternHops()) { + if (!ph.hasFlexService() || ! (ph instanceof FlexPatternHop)) { + continue; + } + FlexPatternHop patternHop = (FlexPatternHop) ph; + List edges; + if (patternHop.getGeometry() == null) { + continue; + } + if (isSinglePoint(patternHop.getGeometry())) { + Coordinate pt = patternHop.getGeometry().getCoordinate(); + edges = findClosestEdges(graph, pt); + } + else { + edges = matcher.match(patternHop.getGeometry()); + } + + if (edges == null || edges.isEmpty()) { + LOG.warn("Could not match to street network: {}", pattern); + continue; + } + for (Edge e : edges) { + hopsForEdge.put(e, patternHop); + } + + // do the reverse, since we are walking and can go the other way. + edges = matcher.match(patternHop.getGeometry().reverse()); + if (edges == null || edges.isEmpty()) { + continue; + } + for (Edge e : edges) { + hopsForEdge.put(e, patternHop); + } + } + } + } + } + + private List findClosestEdges(Graph graph, Coordinate pointLocation) { + if (graph.streetIndex == null) + return Collections.emptyList(); + + final double radiusDeg = SphericalDistanceLibrary.metersToDegrees(500); + + Envelope env = new Envelope(pointLocation); + + // local equirectangular projection + double lat = pointLocation.getOrdinate(1); + final double xscale = Math.cos(lat * Math.PI / 180); + + env.expandBy(radiusDeg / xscale, radiusDeg); + + Collection edges = graph.streetIndex.getEdgesForEnvelope(env); + if (edges.isEmpty()) { + return Collections.emptyList(); + } + Map> edgeDistanceMap = new TreeMap<>(); + for(Edge edge : edges){ + if(edge instanceof StreetEdge){ + LineString line = edge.getGeometry(); + double dist = SphericalDistanceLibrary.fastDistance(pointLocation, line); + double roundOff = (double) Math.round(dist * 100) / 100; + if(!edgeDistanceMap.containsKey(roundOff)) + edgeDistanceMap.put(roundOff, new ArrayList<>()); + edgeDistanceMap.get(roundOff).add((StreetEdge) edge); + } + } + + List closestEdges = edgeDistanceMap.values().iterator().next() + .stream().map(e -> (Edge) e).collect(Collectors.toList()); + return closestEdges; + } + + private boolean isSinglePoint(LineString line) { + Coordinate coord = line.getCoordinate(); + for (int i = 0; i < line.getNumPoints(); i++) { + if (!coord.equals(line.getCoordinateN(i))) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/org/opentripplanner/routing/flex/GtfsFlexGraphModifier.java b/src/main/java/org/opentripplanner/routing/flex/GtfsFlexGraphModifier.java new file mode 100644 index 00000000000..fd979103bf1 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/flex/GtfsFlexGraphModifier.java @@ -0,0 +1,459 @@ +package org.opentripplanner.routing.flex; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import org.apache.commons.math3.util.Pair; +import org.opentripplanner.model.FeedScopedId; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.algorithm.GenericDijkstra; +import org.opentripplanner.routing.algorithm.TraverseVisitor; +import org.opentripplanner.routing.algorithm.strategies.SearchTerminationStrategy; +import org.opentripplanner.routing.algorithm.strategies.TrivialRemainingWeightHeuristic; +import org.opentripplanner.routing.core.RoutingContext; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.core.TraverseMode; +import org.opentripplanner.routing.edgetype.flex.FlexPatternHop; +import org.opentripplanner.routing.edgetype.PatternHop; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.edgetype.flex.FlexTransitBoardAlight; +import org.opentripplanner.routing.edgetype.flex.TemporaryDirectPatternHop; +import org.opentripplanner.routing.edgetype.flex.TemporaryPartialPatternHop; +import org.opentripplanner.routing.edgetype.flex.TemporaryPreAlightEdge; +import org.opentripplanner.routing.edgetype.flex.TemporaryPreBoardEdge; +import org.opentripplanner.routing.edgetype.flex.TemporaryStreetTransitLink; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.Vertex; +import org.opentripplanner.routing.spt.GraphPath; +import org.opentripplanner.routing.vertextype.PatternArriveVertex; +import org.opentripplanner.routing.vertextype.PatternDepartVertex; +import org.opentripplanner.routing.vertextype.PatternStopVertex; +import org.opentripplanner.routing.vertextype.StreetVertex; +import org.opentripplanner.routing.vertextype.TemporaryVertex; +import org.opentripplanner.routing.vertextype.TransitStop; +import org.opentripplanner.routing.vertextype.TransitStopArrive; +import org.opentripplanner.routing.vertextype.TransitStopDepart; +import org.opentripplanner.routing.vertextype.flex.TemporaryPatternArriveVertex; +import org.opentripplanner.routing.vertextype.flex.TemporaryPatternDepartVertex; +import org.opentripplanner.routing.vertextype.flex.TemporaryTransitStop; +import org.opentripplanner.routing.vertextype.flex.TemporaryTransitStopArrive; +import org.opentripplanner.routing.vertextype.flex.TemporaryTransitStopDepart; +import org.opentripplanner.util.I18NString; +import org.opentripplanner.util.LocalizedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.Queue; +import java.util.stream.Collectors; + +/** + * Create temporary vertices and edges for GTFS-flex service. + */ +public abstract class GtfsFlexGraphModifier { + + private static final Logger LOG = LoggerFactory.getLogger(GtfsFlexGraphModifier.class); + + protected Graph graph; + + private Map temporaryTransitStopsForLocation = Maps.newHashMap(); + + protected GtfsFlexGraphModifier(Graph graph) { + this.graph = graph; + } + + /* + * Overall pattern - do a graph search from the origin and destination according to some + * parameters, in order to find nearby PatternHops and the States where they were found. + * Based on those PatternHops/States, make new board and alight temporary edges/vertices. + * Note: this was originally factored out of flag stop creation code. + */ + + /** + * Return the mode that the graph search should be in. + */ + public abstract TraverseMode getMode(); + + /** + * Return the termination strategy which should be used during the graph search. + */ + public abstract SearchTerminationStrategy getSearchTerminationStrategy(); + + // this the core of what's different: how the hops are created + + /** + * Create a new {@link FlexPatternHop} with a new "to" location (from the route to a new destination). + * + * @param opt Options for the graph search + * @param state State at which the original FlexPatternHop was found during the graph search + * @param hop Original pattern hop to modify + * @param to New "to" location + * @param toStop Stop for new "to" location + * @return new pattern hop + */ + public abstract TemporaryPartialPatternHop makeHopNewTo(RoutingRequest opt, State state, FlexPatternHop hop, PatternArriveVertex to, Stop toStop); + + /** + * Create a new {@link FlexPatternHop} with a new "from" location (from a new destination to the route). + * + * @param opt Options for the graph search + * @param state Options for the graph search + * @param hop Original pattern hop to modify + * @param from New "from" location + * @param fromStop Stop for new "from" location + * @return new pattern hop + */ + public abstract TemporaryPartialPatternHop makeHopNewFrom(RoutingRequest opt, State state, FlexPatternHop hop, PatternDepartVertex from, Stop fromStop); + + /** + * From an existing TemporaryPartialPatternHop which has a new "from" location, create a new + * TemporaryPartialPatternHop with the same "from" location and a new "to" location. The + * existence of this method implies that searches from the origin should take place prior + * to searches from the destination. + * + * @param opt Options for the graph search + * @param state ptions for the graph search + * @param hop Temporary pattern hop to modify + * @param to New "to" location + * @param toStop Stop for new "to" location + * @return new pattern hop + */ + public abstract TemporaryPartialPatternHop shortenEnd(RoutingRequest opt, State state, TemporaryPartialPatternHop hop, PatternStopVertex to, Stop toStop); + + /** + * Returns true if the given hop can be boarded/alighted. + */ + public abstract boolean checkHopAllowsBoardAlight(State state, FlexPatternHop hop, boolean boarding); + + /** + * Subclasses can specify where the new temporary stop should be created, given a nearby + * PatternHop. + * + * @param s State at which PatternHop was found + * @param hop PatternHop found during graph search + * @return location for new stop + */ + public abstract StreetVertex getLocationForTemporaryStop(State s, FlexPatternHop hop); + + public void vertexVisitor(State state) {} + + /** + * Create temporary edges and vertices from the origin into the transit network. + * + * @param request request for graph search + */ + public void createForwardHops(RoutingRequest request) { + RoutingRequest forward = request.clone(); + forward.setMode(getMode()); + forward.setArriveBy(false); + // allow discovery of stations even with car mode + forward.enterStationsWithCar = true; + streetSearch(forward); + } + + /** + * Create temporary edges and vertices from the transit network to the destination. + * + * @param request request for graph search + */ + public void createBackwardHops(RoutingRequest request) { + RoutingRequest backward = request.clone(); + backward.setMode(getMode()); + backward.setArriveBy(true); + backward.enterStationsWithCar = true; + streetSearch(backward); + } + + protected void streetSearch(RoutingRequest rr) { + if (TraverseMode.CAR.equals(getMode())) { + modifyRequestForCarAccess(rr); + } + for(Pair p : getClosestPatternHops(rr)) { + State s = p.getKey(); + FlexPatternHop hop = p.getValue(); + TemporaryTransitStop flagTransitStop = getTemporaryStop(s, hop); + if (flagTransitStop == null) { + continue; + } + if (rr.arriveBy) { + createHopsToTemporaryStop(rr, s, flagTransitStop, hop); + } else { + createHopsFromTemporaryStop(rr, s, flagTransitStop, hop); + } + } + } + + private TemporaryTransitStop getTemporaryStop(State s, FlexPatternHop hop) { + StreetVertex streetVertex = getLocationForTemporaryStop(s, hop); + if (streetVertex == null) { + return null; + } + return getTemporaryStop(streetVertex, s, s.getContext(), s.getOptions()); + } + + protected TemporaryTransitStop getTemporaryStop(StreetVertex streetVertex, State s, RoutingContext rctx, RoutingRequest options) { + return getTemporaryStop(streetVertex, s, rctx, options, !options.arriveBy); + } + + protected TemporaryTransitStop getTemporaryStop(StreetVertex streetVertex, State s, RoutingContext rctx, RoutingRequest options, boolean forwards) { + if (temporaryTransitStopsForLocation.get(streetVertex) == null) { + String name = findName(s, streetVertex, options.locale, forwards); + TemporaryTransitStop stop = createTemporaryTransitStop(name, streetVertex, rctx); + temporaryTransitStopsForLocation.put(streetVertex, stop); + return stop; + } + return temporaryTransitStopsForLocation.get(streetVertex); + } + + // Return a reasonable name for a vertex. + private String findName(State state, StreetVertex vertex, Locale locale, boolean forwards) { + I18NString unnamed = new LocalizedString("unnamedStreet", (String[]) null); + I18NString name = vertex.getIntersectionName(locale); + if (!name.equals(unnamed)) { + return name.toString(); + } + // search for street edges but don't look too far away + Queue queue = new LinkedList<>(); + queue.add(vertex); + int n = 0; + while (!queue.isEmpty() && n < 3) { + Vertex v = queue.poll(); + for (Edge e : (forwards ? v.getOutgoing() : v.getIncoming())) { + if (e instanceof StreetEdge) { + return e.getName(locale); + } else { + queue.add(forwards ? e.getToVertex() : e.getFromVertex()); + } + } + n++; + } + if (state != null && state.backEdge instanceof StreetEdge) { // this really assumes flag stops + return state.backEdge.getName(locale); + } + return unnamed.toString(); + } + + private Collection> getClosestPatternHops(RoutingRequest rr) { + Map patternHopStateMap = Maps.newHashMap(); + GenericDijkstra gd = new GenericDijkstra(rr); + gd.setHeuristic(new TrivialRemainingWeightHeuristic()); + gd.traverseVisitor = new TraverseVisitor() { + @Override + public void visitEdge(Edge edge, State state) { + addStateToPatternHopStateMap(edge, state, patternHopStateMap); + } + + @Override + public void visitVertex(State state) { + vertexVisitor(state); + } + + @Override + public void visitEnqueue(State state) { + } + }; + gd.setSearchTerminationStrategy(getSearchTerminationStrategy()); + + + Vertex initVertex = rr.arriveBy ? rr.rctx.toVertex : rr.rctx.fromVertex; + gd.getShortestPathTree(new State(initVertex, rr)); + + return patternHopStateMap.entrySet() + .stream() + .map(e -> new Pair<>(e.getValue(), e.getKey())) + .collect(Collectors.toList()); + } + + private TemporaryTransitStop createTemporaryTransitStop(String name, StreetVertex v, RoutingContext rctx) { + Stop flagStop = new Stop(); + flagStop.setId(new FeedScopedId("1", "temp_" + String.valueOf(Math.random()))); + flagStop.setLat(v.getLat()); + flagStop.setLon(v.getLon()); + flagStop.setName(name); + flagStop.setLocationType(99); + TemporaryTransitStop flagTransitStop = new TemporaryTransitStop(flagStop, v); + rctx.temporaryVertices.add(flagTransitStop); + return flagTransitStop; + } + + private void createHopsToTemporaryStop(RoutingRequest rr, State state, TemporaryTransitStop flagTransitStop, FlexPatternHop originalPatternHop) { + Stop flagStop = flagTransitStop.getStop(); + + TransitStopArrive transitStopArrive = createTransitStopArrive(rr, flagTransitStop); + + Collection reverseHops = findTemporaryPatternHops(rr, originalPatternHop); + for (TemporaryPartialPatternHop reverseHop : reverseHops) { + // create new shortened hop + TemporaryPatternArriveVertex patternArriveVertex = createPatternArriveVertex(rr, originalPatternHop, flagStop); + + TemporaryPartialPatternHop newHop = shortenEnd(rr, state, reverseHop, patternArriveVertex, flagStop); + if (newHop == null || newHop.isTrivial(rr)) { + if (newHop != null) { + removeEdge(newHop); + } + continue; + } + createAlightEdge(rr, transitStopArrive, patternArriveVertex, newHop); + } + + TemporaryPatternArriveVertex patternArriveVertex = createPatternArriveVertex(rr, originalPatternHop, flagStop); + + TemporaryPartialPatternHop hop = makeHopNewTo(rr, state, originalPatternHop, patternArriveVertex, flagStop); + if (hop == null || hop.isTrivial(rr)) { + if (hop != null) { + removeEdge(hop); + } + return; + } + createAlightEdge(rr, transitStopArrive, patternArriveVertex, hop); + } + + private void createHopsFromTemporaryStop(RoutingRequest rr, State state, TemporaryTransitStop flagTransitStop, FlexPatternHop originalPatternHop) { + Stop flagStop = flagTransitStop.getStop(); + + TransitStopDepart transitStopDepart = createTransitStopDepart(rr, flagTransitStop); + + TemporaryPatternDepartVertex patternDepartVertex = createPatternDepartVertex(rr, originalPatternHop, flagStop); + + TemporaryPartialPatternHop hop = makeHopNewFrom(rr, state, originalPatternHop, patternDepartVertex, flagStop); + if (hop == null || hop.isTrivial(rr)) { + if (hop != null) { + removeEdge(hop); + } + return; + } + createBoardEdge(rr, transitStopDepart, patternDepartVertex, hop); + } + + public void createDirectHop(RoutingRequest rr, FlexPatternHop originalPatternHop, TransitStop fromStop, TransitStop toStop, GraphPath path) { + if (fromStop instanceof TemporaryTransitStop && fromStop.departVertex == null) { + createTransitStopDepart(rr, (TemporaryTransitStop) fromStop); + } + if (toStop instanceof TemporaryTransitStop && toStop.arriveVertex == null) { + createTransitStopArrive(rr, (TemporaryTransitStop) toStop); + } + + TemporaryPatternDepartVertex patternDepartVertex = createPatternDepartVertex(rr, originalPatternHop, fromStop.getStop()); + TemporaryPatternArriveVertex patternArriveVertex = createPatternArriveVertex(rr, originalPatternHop, toStop.getStop()); + + // direct hop + TemporaryDirectPatternHop newHop = new TemporaryDirectPatternHop(originalPatternHop, patternDepartVertex, patternArriveVertex, fromStop.getStop(), toStop.getStop(), + path.getGeometry(), path.getDuration()); + + createBoardEdge(rr, fromStop.departVertex, patternDepartVertex, newHop); + createAlightEdge(rr, toStop.arriveVertex, patternArriveVertex, newHop); + } + + private void addStateToPatternHopStateMap(Edge edge, State s, Map patternHopStateMap) { + Collection hops = graph.flexIndex.getHopsForEdge(edge); + for(FlexPatternHop hop : hops){ + if(patternHopStateMap.containsKey(hop)){ + State oldState = patternHopStateMap.get(hop); + if(oldState.getBackState().getWeight() < s.getBackState().getWeight()) { + continue; + } + } + if (checkHopAllowsBoardAlight(s, hop, !s.getOptions().arriveBy)) { + patternHopStateMap.put(hop, s); + } + } + } + + private TransitStopDepart createTransitStopDepart(RoutingRequest rr, TemporaryTransitStop transitStop) { + TransitStopDepart transitStopDepart; + if (transitStop.departVertex == null) { + new TemporaryStreetTransitLink(transitStop.getStreetVertex(), transitStop, true); + + transitStopDepart = new TemporaryTransitStopDepart(transitStop.getStop(), transitStop); + rr.rctx.temporaryVertices.add(transitStopDepart); + new TemporaryPreBoardEdge(transitStop, transitStopDepart); + + transitStop.departVertex = transitStopDepart; + } else { + transitStopDepart = transitStop.departVertex; + } + return transitStopDepart; + } + + private TemporaryPatternDepartVertex createPatternDepartVertex(RoutingRequest rr, PatternHop hop, Stop stop) { + TemporaryPatternDepartVertex patternDepartVertex = + new TemporaryPatternDepartVertex(hop.getPattern(), hop.getStopIndex(), stop); + rr.rctx.temporaryVertices.add(patternDepartVertex); + return patternDepartVertex; + } + + private FlexTransitBoardAlight createBoardEdge(RoutingRequest rr, TransitStopDepart transitStopDepart, PatternDepartVertex patternDepartVertex, + TemporaryPartialPatternHop hop) { + FlexTransitBoardAlight transitBoardAlight = + new FlexTransitBoardAlight(transitStopDepart, patternDepartVertex, hop.getStopIndex(), hop); + return transitBoardAlight; + } + + private FlexTransitBoardAlight createAlightEdge(RoutingRequest rr, TransitStopArrive transitStopArrive, PatternArriveVertex patternArriveVertex, TemporaryPartialPatternHop hop) { + FlexTransitBoardAlight transitBoardAlight = + new FlexTransitBoardAlight(patternArriveVertex, transitStopArrive, hop.getStopIndex() + 1, hop); + return transitBoardAlight; + } + + private TransitStopArrive createTransitStopArrive(RoutingRequest rr, TemporaryTransitStop transitStop) { + TransitStopArrive transitStopArrive; + if (transitStop.arriveVertex == null) { + new TemporaryStreetTransitLink(transitStop, transitStop.getStreetVertex(), true); + + transitStopArrive = new TemporaryTransitStopArrive(transitStop.getStop(), transitStop); + rr.rctx.temporaryVertices.add(transitStopArrive); + new TemporaryPreAlightEdge(transitStopArrive, transitStop); + + transitStop.arriveVertex = transitStopArrive; + } else { + transitStopArrive = transitStop.arriveVertex; + } + return transitStopArrive; + } + + private TemporaryPatternArriveVertex createPatternArriveVertex(RoutingRequest rr, FlexPatternHop hop, Stop stop) { + TemporaryPatternArriveVertex patternArriveVertex = + new TemporaryPatternArriveVertex(hop.getPattern(), hop.getStopIndex() + 1, stop); + rr.rctx.temporaryVertices.add(patternArriveVertex); + return patternArriveVertex; + } + + private Collection findTemporaryPatternHops(RoutingRequest options, FlexPatternHop patternHop) { + Collection edges = new ArrayList(); + for (Vertex vertex : options.rctx.temporaryVertices) { + for (Edge edge : Iterables.concat(vertex.getOutgoing(), vertex.getIncoming())) { + if (edge instanceof TemporaryPartialPatternHop) { + TemporaryPartialPatternHop hop = (TemporaryPartialPatternHop) edge; + if (hop.isOriginalHop(patternHop)) + edges.add(hop); + } + } + } + return edges; + } + + private void modifyRequestForCarAccess(RoutingRequest opt) { + Vertex fromVertex = findCarAccessibleVertex(opt, opt.rctx.fromVertex, false); + Vertex toVertex = findCarAccessibleVertex(opt, opt.rctx.toVertex, true); + Collection temporaryVertices = opt.rctx.temporaryVertices; + opt.setRoutingContext(opt.rctx.graph, fromVertex, toVertex); + opt.rctx.temporaryVertices = temporaryVertices; + } + + private Vertex findCarAccessibleVertex(RoutingRequest opt, Vertex vertex, boolean arriveBy) { + if (vertex instanceof TransitStop && ((TransitStop) vertex).getModes().contains(TraverseMode.BUS)) { + return vertex; + } + return new CarPermissionSearch(opt, arriveBy).findVertexWithPermission(vertex, TraverseMode.CAR); + } + + private void removeEdge(Edge edge) { + edge.getFromVertex().removeOutgoing(edge); + edge.getToVertex().removeIncoming(edge); + } +} diff --git a/src/main/java/org/opentripplanner/routing/graph/Edge.java b/src/main/java/org/opentripplanner/routing/graph/Edge.java index 06cb132f9ec..e03f2e9b0d1 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Edge.java +++ b/src/main/java/org/opentripplanner/routing/graph/Edge.java @@ -1,6 +1,6 @@ package org.opentripplanner.routing.graph; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.model.Trip; import org.opentripplanner.common.MavenVersion; import org.opentripplanner.routing.core.RoutingRequest; @@ -189,6 +189,12 @@ public LineString getGeometry() { return null; } + // Allow subclasses to provide a special geometry for display purposes. Required for flag stop + // partial PatternHops, which have a buffer area to display. + public LineString getDisplayGeometry() { + return getGeometry(); + } + /** * Returns the azimuth of this edge from head to tail. * diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index bb2e5d72ac4..5d45dee22b3 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -1,15 +1,27 @@ package org.opentripplanner.routing.graph; - +import com.conveyal.kryo.TIntArrayListSerializer; +import com.conveyal.kryo.TIntIntHashMapSerializer; +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.esotericsoftware.kryo.serializers.ExternalizableSerializer; +import com.esotericsoftware.kryo.serializers.JavaSerializer; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.*; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Polygon; +import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer; +import gnu.trove.impl.hash.TPrimitiveHash; import gnu.trove.list.TDoubleList; +import gnu.trove.list.array.TIntArrayList; import gnu.trove.list.linked.TDoubleLinkedList; +import gnu.trove.map.hash.TIntIntHashMap; import org.apache.commons.math3.stat.descriptive.rank.Median; import org.joda.time.DateTime; +import org.objenesis.strategy.SerializingInstantiatorStrategy; import org.opentripplanner.calendar.impl.CalendarServiceImpl; import org.opentripplanner.graph_builder.module.GraphBuilderModuleSummary; import org.opentripplanner.model.Agency; @@ -26,6 +38,7 @@ import org.opentripplanner.common.geometry.GraphUtils; import org.opentripplanner.graph_builder.annotation.GraphBuilderAnnotation; import org.opentripplanner.graph_builder.annotation.NoFutureDates; +import org.opentripplanner.kryo.HashBiMapSerializer; import org.opentripplanner.model.GraphBundle; import org.opentripplanner.profile.StopClusterMode; import org.opentripplanner.routing.alertpatch.AlertPatch; @@ -35,12 +48,12 @@ import org.opentripplanner.routing.edgetype.EdgeWithCleanup; import org.opentripplanner.routing.edgetype.StreetEdge; import org.opentripplanner.routing.edgetype.TripPattern; +import org.opentripplanner.routing.flex.FlexIndex; import org.opentripplanner.routing.services.StreetVertexIndexService; import org.opentripplanner.routing.services.notes.StreetNotesService; import org.opentripplanner.routing.trippattern.Deduplicator; import org.opentripplanner.routing.vertextype.PatternArriveVertex; import org.opentripplanner.routing.vertextype.TransitStop; -import org.opentripplanner.traffic.StreetSpeedSnapshotSource; import org.opentripplanner.updater.GraphUpdaterConfigurator; import org.opentripplanner.updater.GraphUpdaterManager; import org.opentripplanner.updater.stoptime.TimetableSnapshotSource; @@ -88,8 +101,6 @@ public class Graph implements Serializable { private transient CalendarService calendarService; - private boolean debugData = true; - // TODO this would be more efficient if it was just an array. private transient Map vertexById; @@ -99,15 +110,17 @@ public class Graph implements Serializable { public transient GraphIndex index; + public transient FlexIndex flexIndex; + private transient GeometryIndex geomIndex; private transient SampleFactory sampleFactory; - public final Deduplicator deduplicator = new Deduplicator(); + public final transient Deduplicator deduplicator = new Deduplicator(); /** * Map from GTFS ServiceIds to integers close to 0. Allows using BitSets instead of Set. - * An empty Map is created before the Graph is built to allow registering IDs from multiple feeds. + * An empty Map is created before the Graph is built to allow registering IDs from multiple feeds. */ public final Map serviceCodes = Maps.newHashMap(); @@ -188,9 +201,6 @@ public class Graph implements Serializable { /** Has information how much time alighting a vehicle takes. Can be significant eg in airplanes or ferries. */ public Map alightTimes = Collections.EMPTY_MAP; - /** A speed source for traffic data */ - public transient StreetSpeedSnapshotSource streetSpeedSource; - /** How should we cluster stops? By 'proximity' or 'ParentStation' */ public StopClusterMode stopClusterMode = StopClusterMode.proximity; @@ -203,6 +213,12 @@ public class Graph implements Serializable { /** used during graph build to store known elevations */ private transient HashMap knownElevations; + /** Whether to use flex modes */ + public boolean useFlexService = false; + + /** Areas for flex service */ + public Map flexAreasById = new HashMap<>(); + public Graph(Graph basedOn) { this(); this.bundle = basedOn.getBundle(); @@ -237,8 +253,8 @@ public void addVertex(Vertex v) { public void removeVertex(Vertex v) { if (vertices.remove(v.getLabel()) != v) { LOG.error( - "attempting to remove vertex that is not in graph (or mapping value was null): {}", - v); + "attempting to remove vertex that is not in graph (or mapping value was null): {}", + v); } } @@ -452,8 +468,8 @@ public List getTurnRestrictions(Edge edge) { public Collection getStreetEdges() { Collection allEdges = this.getEdges(); return Lists.newArrayList(Iterables.filter(allEdges, StreetEdge.class)); - } - + } + public boolean containsVertex(Vertex v) { return (v != null) && vertices.get(v.getLabel()) == v; } @@ -572,7 +588,7 @@ public int countVertices() { /** * Find the total number of edges in this Graph. There are assumed to be no Edges in an incoming edge list that are not in an outgoing edge list. - * + * * @return number of outgoing edges in the graph */ public int countEdges() { @@ -592,13 +608,13 @@ private void addEdgesToIndex(Collection es) { this.edgeById.put(e.getId(), e); } } - + /** * Rebuilds any indices on the basis of current vertex and edge IDs. - * - * If you want the index to be accurate, you must run this every time the + * + * If you want the index to be accurate, you must run this every time the * vertex or edge set changes. - * + * * TODO(flamholz): keep the indices up to date with changes to the graph. * This is not simple because the Vertex constructor may add itself to the graph * before the Vertex has any edges, so updating indices on addVertex is insufficient. @@ -624,7 +640,7 @@ public void rebuildVertexAndEdgeIndices() { } private void readObject(ObjectInputStream inputStream) throws ClassNotFoundException, - IOException { + IOException { inputStream.defaultReadObject(); } @@ -632,7 +648,7 @@ private void readObject(ObjectInputStream inputStream) throws ClassNotFoundExcep * Add a graph builder annotation to this graph's list of graph builder annotations. The return value of this method * is the annotation's message, which allows for a single-line idiom that creates, registers, and logs a new graph * builder annotation: log.warning(graph.addBuilderAnnotation(new SomeKindOfAnnotation(param1, param2))); - * + * * If the graphBuilderAnnotations field of this graph is null, the annotation is not actually saved, but the message * is still returned. This allows annotation registration to be turned off, saving memory and disk space when the * user is not interested in annotations. @@ -678,31 +694,12 @@ public void setKnownElevations(HashMap knownElevations) { /* (de) serialization */ - public enum LoadLevel { - BASIC, FULL, DEBUG; - } - - public static Graph load(File file, LoadLevel level) throws IOException, ClassNotFoundException { + public static Graph load(File file) throws IOException { LOG.info("Reading graph " + file.getAbsolutePath() + " ..."); - // cannot use getClassLoader() in static context - ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); - return load(in, level); - } - - public static Graph load(ClassLoader classLoader, File file, LoadLevel level) - throws IOException, ClassNotFoundException { - LOG.info("Reading graph " + file.getAbsolutePath() + " with alternate classloader ..."); - ObjectInputStream in = new GraphObjectInputStream(new BufferedInputStream( - new FileInputStream(file)), classLoader); - return load(in, level); + return load(new FileInputStream(file)); } - public static Graph load(InputStream is, LoadLevel level) throws ClassNotFoundException, - IOException { - return load(new ObjectInputStream(is), level); - } - - /** + /** * Perform indexing on vertices, edges, and timetables, and create transient data structures. This used to be done * in readObject methods upon deserialization, but stand-alone mode now allows passing graphs from graphbuilder to * server in memory, without a round trip through serialization. @@ -749,63 +746,48 @@ public void index (boolean recreateStreetIndex) { } // TODO: Move this ^ stuff into the graph index this.index = new GraphIndex(this); - LOG.info("Done rebuilding edge and vertex indices"); + if (useFlexService ) { + this.flexIndex = new FlexIndex(); + flexIndex.init(this); + } } - - /** - * Loading which allows you to inject other implementation. - * @param in - * @param level - * @return - * @throws IOException - * @throws ClassNotFoundException - */ - @SuppressWarnings("unchecked") - public static Graph load(ObjectInputStream in, LoadLevel level) throws IOException, ClassNotFoundException { - try { - Graph graph = (Graph) in.readObject(); - LOG.debug("Basic graph info read."); - if (graph.graphVersionMismatch()) - throw new RuntimeException("Graph version mismatch detected."); - if (level == LoadLevel.BASIC) - return graph; - // vertex edge lists are transient to avoid excessive recursion depth - // vertex list is transient because it can be reconstructed from edges - LOG.debug("Loading edges..."); - List edges = (ArrayList) in.readObject(); - graph.vertices = new HashMap(); - - for (Edge e : edges) { - graph.vertices.put(e.getFromVertex().getLabel(), e.getFromVertex()); - graph.vertices.put(e.getToVertex().getLabel(), e.getToVertex()); - } - LOG.info("Main graph read. |V|={} |E|={}", graph.countVertices(), graph.countEdges()); - // Make sure the graph index has been initialized. The streetIndex should be able to be safely recreated - // because the streetIndexes used during build will not be visible outside of this method. - graph.index(true); - - if (level == LoadLevel.FULL) { - return graph; - } - - if (graph.debugData) { - graph.graphBuilderAnnotations = (List) in.readObject(); - LOG.debug("Debug info read."); - } else { - LOG.warn("Graph file does not contain debug data."); - } - return graph; - } catch (InvalidClassException ex) { - LOG.error("Stored graph is incompatible with this version of OTP, please rebuild it."); - throw new IllegalStateException("Stored Graph version error", ex); + public static Graph load(InputStream in) { + // TODO store version information, halt load if versions mismatch + Input input = new Input(in); + Kryo kryo = makeKryo(); + Graph graph = (Graph) kryo.readClassAndObject(input); + LOG.debug("Basic graph info read."); + if (graph.graphVersionMismatch()) { + throw new RuntimeException("Graph version mismatch detected."); } + // Vertex edge lists are transient to avoid excessive recursion depth during serialization. + // vertex list is transient because it can be reconstructed from edges. + LOG.debug("Loading edges..."); + List edges = (ArrayList) kryo.readClassAndObject(input); + graph.vertices = new ConcurrentHashMap<>(); // why is this concurrent? + + for (Edge e : edges) { + Vertex fromVertex = e.getFromVertex(); + Vertex toVertex = e.getToVertex(); + graph.vertices.put(fromVertex.getLabel(), fromVertex); + graph.vertices.put(toVertex.getLabel(), toVertex); + // Compensating for the fact that we're not using the standard Java de/serialization methods. + fromVertex.initEdgeListsIfNeeded(); + toVertex.initEdgeListsIfNeeded(); + fromVertex.addOutgoing(e); + toVertex.addIncoming(e); + } + + LOG.info("Main graph read. |V|={} |E|={}", graph.countVertices(), graph.countEdges()); + graph.index(true); + return graph; } /** * Compares the OTP version number stored in the graph with that of the currently running instance. Logs warnings explaining that mismatched * versions can cause problems. - * + * * @return false if Maven versions match (even if commit ids do not match), true if Maven version of graph does not match this version of OTP or * graphs are otherwise obviously incompatible. */ @@ -820,12 +802,12 @@ private boolean graphVersionMismatch() { } else if (!v.commit.equals(gv.commit)) { if (v.qualifier.equals("SNAPSHOT")) { LOG.warn("This graph was built with the same SNAPSHOT version of OTP, but a " - + "different commit. Please rebuild the graph if you experience incorrect " - + "behavior. "); + + "different commit. Please rebuild the graph if you experience incorrect " + + "behavior. "); return false; // graph might still work } else { LOG.error("Commit mismatch in non-SNAPSHOT version. This implies a problem with " - + "the build or release process."); + + "the build or release process."); return true; // major problem } } else { @@ -835,23 +817,65 @@ private boolean graphVersionMismatch() { } } + /** + * This method allows reproducibly creating Kryo (de)serializer instances with exactly the same configuration. + * This allows us to use identically configured instances for serialization and deserialization. + * + * When configuring serializers, there's a difference between kryo.register() and kryo.addDefaultSerializer(). + * The latter will set the default for a whole tree of classes. The former matches only the specified class. + * By default Kryo will serialize all the non-transient fields of an instance. If the class has its own overridden + * Java serialization methods Kryo will not automatically use those, a JavaSerializer must be registered. + */ + public static Kryo makeKryo() { + // For generating a histogram of serialized classes with associated serializers: + // Kryo kryo = new Kryo(new InstanceCountingClassResolver(), new MapReferenceResolver(), new DefaultStreamFactory()); + Kryo kryo = new Kryo(); + // Allow serialization of unrecognized classes, for which we haven't manually set up a serializer. + // We might actually want to manually register a serializer for every class, to be safe. + kryo.setRegistrationRequired(false); + kryo.setReferences(true); + kryo.addDefaultSerializer(TPrimitiveHash.class, ExternalizableSerializer.class); + kryo.register(TIntArrayList.class, new TIntArrayListSerializer()); + kryo.register(TIntIntHashMap.class, new TIntIntHashMapSerializer()); + // Kryo's default instantiation and deserialization of BitSets leaves them empty. + // The Kryo BitSet serializer in magro/kryo-serializers naively writes out a dense stream of booleans. + // BitSet's built-in Java serializer saves the internal bitfields, which is efficient. We use that one. + kryo.register(BitSet.class, new JavaSerializer()); + // BiMap has a constructor that uses its putAll method, which just puts each item in turn. + // It should be possible to reconstruct this like a standard Map. However, the HashBiMap constructor calls an + // init method that creates the two internal maps. So we have to subclass the generic Map serializer. + kryo.register(HashBiMap.class, new HashBiMapSerializer()); + // OBA uses unmodifiable collections, but those classes have package-private visibility. Workaround. + // FIXME we're importing all the contributed kryo-serializers just for this one serializer + try { + Class unmodifiableCollection = Class.forName("java.util.Collections$UnmodifiableCollection"); + kryo.addDefaultSerializer(unmodifiableCollection , UnmodifiableCollectionsSerializer.class); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + // Instantiation strategy: how should Kryo make new instances of objects when they are deserialized? + // The default strategy requires every class you serialize, even in your dependencies, to have a zero-arg + // constructor (which can be private). The setInstantiatorStrategy method completely replaces that default + // strategy. The nesting below specifies the Java approach as a fallback strategy to the default strategy. + kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new SerializingInstantiatorStrategy())); + return kryo; + } + public void save(File file) throws IOException { LOG.info("Main graph size: |V|={} |E|={}", this.countVertices(), this.countEdges()); LOG.info("Writing graph " + file.getAbsolutePath() + " ..."); - ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream( - new FileOutputStream(file))); try { - save(out); - out.close(); - } catch (RuntimeException e) { - out.close(); + save(new FileOutputStream(file)); + } catch (Exception e) { file.delete(); // remove half-written file throw e; } } - public void save(ObjectOutputStream out) throws IOException { + public void save(OutputStream outputStream) { + Kryo kryo = makeKryo(); LOG.debug("Consolidating edges..."); + Output output = new Output(outputStream); // this is not space efficient List edges = new ArrayList(this.countEdges()); for (Vertex v : getVertices()) { @@ -864,37 +888,12 @@ public void save(ObjectOutputStream out) throws IOException { LOG.debug("Assigning vertex/edge ID numbers..."); this.rebuildVertexAndEdgeIndices(); LOG.debug("Writing edges..."); - out.writeObject(this); - out.writeObject(edges); - if (debugData) { - // should we make debug info generation conditional? - LOG.debug("Writing debug data..."); - out.writeObject(this.graphBuilderAnnotations); - out.writeObject(this.vertexById); - out.writeObject(this.edgeById); - } else { - LOG.debug("Skipping debug data."); - } + kryo.writeClassAndObject(output, this); + kryo.writeClassAndObject(output, edges); + output.close(); LOG.info("Graph written."); - } - - /* deserialization for org.opentripplanner.customize */ - private static class GraphObjectInputStream extends ObjectInputStream { - ClassLoader classLoader; - - public GraphObjectInputStream(InputStream in, ClassLoader classLoader) throws IOException { - super(in); - this.classLoader = classLoader; - } - - @Override - public Class resolveClass(ObjectStreamClass osc) { - try { - return Class.forName(osc.getName(), false, classLoader); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } + // Summarize serialized classes and associated serializers: + // ((InstanceCountingClassResolver) kryo.getClassResolver()).summarize(); } public Integer getIdForEdge(Edge edge) { @@ -949,6 +948,10 @@ public void addFeedInfo(FeedInfo info) { this.feedInfoForId.put(info.getId().toString(), info); } + public void addFlexArea(String feedId, String areaId, Polygon flexArea) { + this.flexAreasById.put(new FeedScopedId(feedId, areaId), flexArea); + } + /** * Returns the time zone for the first agency in this graph. This is used to interpret times in API requests. The JVM default time zone cannot be * used because we support multiple graphs on one server via the routerId. Ideally we would want to interpret times in the time zone of the @@ -979,7 +982,24 @@ public TimeZone getTimeZone() { } return timeZone; } - + + /** + * Return all TimeZones for all agencies in the graph + * @return collection of referenced timezones + */ + public Collection getAllTimeZones() { + List timeZones = new ArrayList<>(); + for (String feedId : getFeedIds()) { + for (Agency agency : getAgencies(feedId)) { + TimeZone timeZone = calendarService.getTimeZoneForAgencyId(agency.getId()); + if (timeZone != null) { + timeZones.add(timeZone); + } + } + } + return timeZones; + } + /** * The timezone is cached by the graph. If you've done something to the graph that has the * potential to change the time zone, you should call this to ensure it is reset. @@ -1053,11 +1073,11 @@ public WorldEnvelope getEnvelope() { // lazy-init geom index on an as needed basis public GeometryIndex getGeomIndex() { - - if(this.geomIndex == null) - this.geomIndex = new GeometryIndex(this); - - return this.geomIndex; + + if(this.geomIndex == null) + this.geomIndex = new GeometryIndex(this); + + return this.geomIndex; } // lazy-init sample factor on an as needed basis @@ -1065,7 +1085,7 @@ public SampleFactory getSampleFactory() { if(this.sampleFactory == null) this.sampleFactory = new SampleFactory(this); - return this.sampleFactory; + return this.sampleFactory; } /** @@ -1076,7 +1096,6 @@ public SampleFactory getSampleFactory() { * * This speeds up calculation, but problem is that median needs to have all of latitudes/longitudes * in memory, this can become problematic in large installations. It works without a problem on New York State. - * @see GraphEnvelope */ public void calculateTransitCenter() { if (hasTransit) { @@ -1113,4 +1132,13 @@ public long getTransitServiceStarts() { public long getTransitServiceEnds() { return transitServiceEnds; } + + public void setUseFlexService(boolean useFlexService) { + // when passing in graph from memory, router config had not loaded when "index()" called + if (useFlexService && !this.useFlexService) { + this.flexIndex = new FlexIndex(); + flexIndex.init(this); + } + this.useFlexService = useFlexService; + } } diff --git a/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java b/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java index ce035c884aa..4d26c746aaf 100644 --- a/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java +++ b/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java @@ -1,6 +1,8 @@ package org.opentripplanner.routing.graph; import com.google.common.collect.ArrayListMultimap; + +import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; import java.util.List; @@ -14,8 +16,9 @@ import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.execution.ExecutorServiceExecutionStrategy; @@ -60,7 +63,6 @@ import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response; -import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -97,6 +99,7 @@ public class GraphIndex { final HashGridSpatialIndex stopSpatialIndex = new HashGridSpatialIndex(); public final Map stopClusterForStop = Maps.newHashMap(); public final Map stopClusterForId = Maps.newHashMap(); + public final Map flexAreasById = Maps.newHashMap(); /* Should eventually be replaced with new serviceId indexes. */ private final CalendarService calendarService; @@ -159,6 +162,7 @@ public GraphIndex (Graph graph) { Envelope envelope = new Envelope(stopVertex.getCoordinate()); stopSpatialIndex.insert(envelope, stopVertex); } + for (TripPattern pattern : patternForId.values()) { patternsForFeedId.put(pattern.getFeedId(), pattern); patternsForRoute.put(pattern.route, pattern); @@ -184,6 +188,14 @@ public GraphIndex (Graph graph) { new ExecutorServiceExecutionStrategy(Executors.newCachedThreadPool( new ThreadFactoryBuilder().setNameFormat("GraphQLExecutor-" + graph.routerId + "-%d").build() ))); + + LOG.info("Initializing areas...."); + if (graph.flexAreasById != null) { + for (FeedScopedId id : graph.flexAreasById.keySet()) { + flexAreasById.put(id, graph.flexAreasById.get(id)); + } + } + LOG.info("Done indexing graph."); } @@ -696,5 +708,4 @@ public Set getAllAgencies() { } return allAgencies; } - } diff --git a/src/main/java/org/opentripplanner/routing/graph/Vertex.java b/src/main/java/org/opentripplanner/routing/graph/Vertex.java index ce007db21a0..9c84796d99d 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Vertex.java +++ b/src/main/java/org/opentripplanner/routing/graph/Vertex.java @@ -15,7 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import java.util.Locale; import org.opentripplanner.util.I18NString; import org.opentripplanner.util.NonLocalizedString; @@ -85,6 +85,17 @@ public int hashCode() { return index; } + // Stupid method for deserialization, initialize transient fields. + // Stopgap until old serialization methods are completely replaced. + public void initEdgeListsIfNeeded () { + if (this.outgoing == null) { + this.outgoing = new Edge[0]; + } + if (this.incoming == null) { + this.incoming = new Edge[0]; + } + } + /* EDGE UTILITY METHODS (use arrays to eliminate copy-on-write set objects) */ /** diff --git a/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java index ad091f9f7cd..6005b51c085 100644 --- a/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java @@ -31,7 +31,9 @@ /** A set of edges on a single route, with associated information for calculating fares */ class Ride { - + + String feedId; + String agency; // route agency FeedScopedId route; @@ -177,6 +179,7 @@ protected List createRides(GraphPath path) { ride.startTime = state.getBackState().getTimeSeconds(); ride.firstStop = hEdge.getBeginStop(); ride.trip = state.getTripId(); + ride.feedId = hEdge.getFeedId(); } ride.lastStop = hEdge.getEndStop(); ride.endZone = ride.lastStop.getZoneId(); @@ -338,12 +341,11 @@ private FareAndId getBestFareAndId(FareType fareType, List rides, long startTime = firstRide.startTime; String startZone = firstRide.startZone; String endZone = firstRide.endZone; - // stops don't really have an agency id, they have the per-feed default id - String feedId = firstRide.firstStop.getId().getAgencyId(); + String feedId = firstRide.feedId; long lastRideStartTime = firstRide.startTime; long lastRideEndTime = firstRide.endTime; for (Ride ride : rides) { - if ( ! ride.firstStop.getId().getAgencyId().equals(feedId)) { + if ( ! ride.feedId.equals(feedId)) { LOG.debug("skipped multi-feed ride sequence {}", rides); return new FareAndId(Float.POSITIVE_INFINITY, null); } diff --git a/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceImpl.java index eafc15a7c33..b58e02ffb7b 100644 --- a/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceImpl.java @@ -24,13 +24,24 @@ public DutchFareServiceImpl(Collection regularFareRules) { private static final Logger LOG = LoggerFactory.getLogger(DutchFareServiceImpl.class); public static final int TRANSFER_DURATION = 60 * 35; /* tranfers within 35 min won't require a new base fare */ + + final Currency euros = Currency.getInstance("EUR"); + /** + * This overridden method completely ignores the Currency object supplied by the caller. + * This is because the caller in the superclass assumes the input data uses only one currency. + * However, Dutch data contains fares in both Euros and Dutch Railways fare units, with the added complexity + * that these pseudo-currency units do not have sub-units in the way Euros have cents, which leads to + * incorrect rounding and scaling etc. While the fare rules consulted by this fare service do have a mix of EUR + * and train pseudo-units, this Fare object is accumulating the monetary fare returned to the user and is known + * to always be in Euros. See issue #2679 for discussion. + */ @Override protected boolean populateFare(Fare fare, Currency currency, FareType fareType, List rides, Collection fareRules) { float lowestCost = getLowestCost(fareType, rides, fareRules); if(lowestCost != Float.POSITIVE_INFINITY) { - fare.addFare(fareType, getMoney(currency, lowestCost)); + fare.addFare(fareType, getMoney(euros, lowestCost)); return true; } return false; @@ -87,7 +98,7 @@ public UnitsFareZone(int units, String fareZone) { private UnitsFareZone getUnitsByZones(String agencyId, String startZone, String endZone, Collection fareRules) { P2 od = new P2(startZone, endZone); - LOG.warn("Search " + startZone + " and " + endZone); + LOG.trace("Search " + startZone + " and " + endZone); String fareIdStartsWith = agencyId + "::"; @@ -98,7 +109,7 @@ private UnitsFareZone getUnitsByZones(String agencyId, String startZone, String String[] parts = fareId.split("::"); String fareZone = parts[1]; - LOG.warn("Between " + startZone + " and " + endZone + ": " + (int) ruleSet.getFareAttribute().getPrice() + " (" + fareZone + ")"); + LOG.trace("Between " + startZone + " and " + endZone + ": " + (int) ruleSet.getFareAttribute().getPrice() + " (" + fareZone + ")"); return new UnitsFareZone((int) ruleSet.getFareAttribute().getPrice(), fareZone); } } @@ -210,10 +221,10 @@ protected float getLowestCost(FareType fareType, List rides, Collection rides, Collection rides, Collection rides, Collection rides, Collection rides, Collection getPaths(RoutingRequest options) { */ if (options.maxWalkDistance == Double.MAX_VALUE) options.maxWalkDistance = DEFAULT_MAX_WALK; if (options.maxWalkDistance > CLAMP_MAX_WALK) options.maxWalkDistance = CLAMP_MAX_WALK; + if (options.modes.isTransit() && router.graph.useFlexService) { + // create temporary flex stops/hops (just once even if we run multiple searches) + FlagStopGraphModifier flagStopGraphModifier = new FlagStopGraphModifier(router.graph); + DeviatedRouteGraphModifier deviatedRouteGraphModifier = new DeviatedRouteGraphModifier(router.graph); + flagStopGraphModifier.createForwardHops(options); + if (options.flexUseReservationServices) { + deviatedRouteGraphModifier.createForwardHops(options); + } + flagStopGraphModifier.createBackwardHops(options); + if (options.flexUseReservationServices) { + deviatedRouteGraphModifier.createBackwardHops(options); + } + } long searchBeginTime = System.currentTimeMillis(); LOG.debug("BEGIN SEARCH"); List paths = Lists.newArrayList(); @@ -173,13 +188,28 @@ public List getPaths(RoutingRequest options) { for (GraphPath path : newPaths) { // path.dump(); List tripIds = path.getTrips(); + List callAndRideTripIds = path.getCallAndRideTrips(); for (FeedScopedId tripId : tripIds) { - options.banTrip(tripId); + if (!callAndRideTripIds.contains(tripId)) { + options.banTrip(tripId); + } } if (tripIds.isEmpty()) { // This path does not use transit (is entirely on-street). Do not repeatedly find the same one. options.onlyTransitTrips = true; } + // Call-and-Ride trips should not use regular trip-banning, since call-and-ride trips can beused in + // multiple ways (e.g. from origin to destination, or from origin to a transfer stop.) Instead, + // after an itinerary which uses call-and-ride is found, reduce the allowable call-and-ride duration + // so that the same leg cannot be found in a subsequent search. + if (tripIds.size() < 2) { + int duration = path.getCallAndRideDuration(); + if (duration > 0) { // only true if there are call-and-ride legs + int constantLimit = Math.min(0, duration - options.flexReduceCallAndRideSeconds); + int ratioLimit = (int) Math.round(options.flexReduceCallAndRideRatio * duration); + options.flexMaxCallAndRideSeconds = Math.min(constantLimit, ratioLimit); + } + } } paths.addAll(newPaths.stream() @@ -395,6 +425,7 @@ public List graphPathFinderEntryPoint (RoutingRequest request) { * the end. */ private List getGraphPathsConsideringIntermediates (RoutingRequest request) { + Collection temporaryVertices = new ArrayList<>(); if (request.hasIntermediatePlaces()) { List places = Lists.newArrayList(request.from); places.addAll(request.intermediatePlaces); @@ -412,7 +443,7 @@ private List getGraphPathsConsideringIntermediates (RoutingRequest re intermediateRequest.from = places.get(placeIndex - 1); intermediateRequest.to = places.get(placeIndex); intermediateRequest.rctx = null; - intermediateRequest.setRoutingContext(router.graph); + intermediateRequest.setRoutingContext(router.graph, temporaryVertices); if (debugOutput != null) {// Restore the previous debug info accumulator intermediateRequest.rctx.debugOutput = debugOutput; diff --git a/src/main/java/org/opentripplanner/routing/impl/GraphScanner.java b/src/main/java/org/opentripplanner/routing/impl/GraphScanner.java index 97c9f08441f..6eb0700978b 100644 --- a/src/main/java/org/opentripplanner/routing/impl/GraphScanner.java +++ b/src/main/java/org/opentripplanner/routing/impl/GraphScanner.java @@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit; import org.opentripplanner.routing.error.GraphNotFoundException; -import org.opentripplanner.routing.graph.Graph.LoadLevel; import org.opentripplanner.routing.services.GraphService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,9 +34,6 @@ public class GraphScanner { /** The default router, none by default */ public String defaultRouterId = null; - /** Load level */ - public LoadLevel loadLevel = LoadLevel.FULL; - /** The GraphService where register graphs to */ private GraphService graphService; @@ -69,7 +65,7 @@ public void startup() { LOG.info("Graph files will be sought in paths relative to {}", basePath); for (String routerId : routerIds) { InputStreamGraphSource graphSource = InputStreamGraphSource.newFileGraphSource( - routerId, getBasePath(routerId), loadLevel); + routerId, getBasePath(routerId)); graphService.registerGraph(routerId, graphSource); } } else { @@ -119,7 +115,7 @@ private void autoScan() { Arrays.toString(graphToRegister.toArray())); for (String routerId : graphToRegister) { InputStreamGraphSource graphSource = InputStreamGraphSource.newFileGraphSource( - routerId, getBasePath(routerId), loadLevel); + routerId, getBasePath(routerId)); // Can be null here if the file has been removed in the meantime. graphService.registerGraph(routerId, graphSource); } diff --git a/src/main/java/org/opentripplanner/routing/impl/InputStreamGraphSource.java b/src/main/java/org/opentripplanner/routing/impl/InputStreamGraphSource.java index 2b8df13784b..93b6dcf8cb5 100644 --- a/src/main/java/org/opentripplanner/routing/impl/InputStreamGraphSource.java +++ b/src/main/java/org/opentripplanner/routing/impl/InputStreamGraphSource.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.node.MissingNode; import com.google.common.io.ByteStreams; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.graph.Graph.LoadLevel; import org.opentripplanner.routing.services.GraphSource; import org.opentripplanner.standalone.Router; import org.slf4j.Logger; @@ -17,7 +16,7 @@ /** * The primary implementation of the GraphSource interface. The graph is loaded from a serialized * graph from a given source. - * + * */ public class InputStreamGraphSource implements GraphSource { @@ -38,8 +37,6 @@ public class InputStreamGraphSource implements GraphSource { private long graphLastModified = 0L; - private LoadLevel loadLevel; - private Object preEvictMutex = new Boolean(false); /** @@ -48,32 +45,22 @@ public class InputStreamGraphSource implements GraphSource { private Streams streams; /** - * @param routerId - * @param path - * @param loadLevel * @return A GraphSource loading graph from the file system under a base path. */ - public static InputStreamGraphSource newFileGraphSource(String routerId, File path, - LoadLevel loadLevel) { - return new InputStreamGraphSource(routerId, loadLevel, new FileStreams(path)); + public static InputStreamGraphSource newFileGraphSource(String routerId, File path) { + return new InputStreamGraphSource(routerId, new FileStreams(path)); } /** - * @param routerId - * @param path - * @param loadLevel * @return A GraphSource loading graph from an embedded classpath resources (a graph bundled * inside a pre-packaged WAR for example). */ - public static InputStreamGraphSource newClasspathGraphSource(String routerId, File path, - LoadLevel loadLevel) { - return new InputStreamGraphSource(routerId, loadLevel, new ClasspathStreams(path)); + public static InputStreamGraphSource newClasspathGraphSource(String routerId, File path) { + return new InputStreamGraphSource(routerId, new ClasspathStreams(path)); } - private InputStreamGraphSource(String routerId, LoadLevel loadLevel, - Streams streams) { + private InputStreamGraphSource(String routerId, Streams streams) { this.routerId = routerId; - this.loadLevel = loadLevel; this.streams = streams; } @@ -151,7 +138,7 @@ public boolean reload(boolean force, boolean preEvict) { /** * Check if a graph has been modified since the last time it has been loaded. - * + * * @param lastModified Time of last modification of current loaded data. * @return True if the input data has been modified and need to be reloaded. */ @@ -159,8 +146,8 @@ private boolean checkAutoReload(long lastModified) { // We check only for graph file modification, not config long validEndTime = System.currentTimeMillis() - LOAD_DELAY_SEC * 1000; LOG.debug( - "checkAutoReload router '{}' validEndTime={} lastModified={} graphLastModified={}", - routerId, validEndTime, lastModified, graphLastModified); + "checkAutoReload router '{}' validEndTime={} lastModified={} graphLastModified={}", + routerId, validEndTime, lastModified, graphLastModified); if (lastModified != graphLastModified && lastModified <= validEndTime) { // Only reload graph modified more than 1 mn ago. LOG.info("Router ID '{}' graph input modification detected, force reload.", routerId); @@ -189,7 +176,7 @@ private Router loadGraph() { try (InputStream is = streams.getGraphInputStream()) { LOG.info("Loading graph..."); try { - newGraph = Graph.load(new ObjectInputStream(is), loadLevel); + newGraph = Graph.load(is); } catch (Exception ex) { LOG.error("Exception while loading graph '{}'.", routerId, ex); return null; @@ -284,7 +271,7 @@ public InputStream getGraphInputStream() { File graphFile = new File(path, GRAPH_FILENAME); LOG.debug("Loading graph from classpath at '{}'", graphFile.getPath()); return Thread.currentThread().getContextClassLoader() - .getResourceAsStream(graphFile.getPath()); + .getResourceAsStream(graphFile.getPath()); } @Override @@ -292,7 +279,7 @@ public InputStream getConfigInputStream() { File configFile = new File(path, Router.ROUTER_CONFIG_FILENAME); LOG.debug("Trying to load config on classpath at '{}'", configFile.getPath()); return Thread.currentThread().getContextClassLoader() - .getResourceAsStream(configFile.getPath()); + .getResourceAsStream(configFile.getPath()); } /** @@ -314,16 +301,13 @@ public static class FileFactory implements GraphSource.Factory { public File basePath; - public LoadLevel loadLevel = LoadLevel.FULL; - public FileFactory(File basePath) { this.basePath = basePath; } @Override public GraphSource createGraphSource(String routerId) { - return InputStreamGraphSource.newFileGraphSource(routerId, getBasePath(routerId), - loadLevel); + return InputStreamGraphSource.newFileGraphSource(routerId, getBasePath(routerId)); } @Override diff --git a/src/main/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImpl.java index 14333ffbb87..fd9c1478eaf 100644 --- a/src/main/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImpl.java @@ -22,8 +22,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; /** * Graph service for depart-on-board mode. diff --git a/src/main/java/org/opentripplanner/routing/location/StreetLocation.java b/src/main/java/org/opentripplanner/routing/location/StreetLocation.java index 50bf1d97b31..057b19c91e6 100644 --- a/src/main/java/org/opentripplanner/routing/location/StreetLocation.java +++ b/src/main/java/org/opentripplanner/routing/location/StreetLocation.java @@ -1,6 +1,6 @@ package org.opentripplanner.routing.location; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import org.opentripplanner.routing.vertextype.StreetVertex; import org.opentripplanner.util.I18NString; import org.opentripplanner.util.NonLocalizedString; diff --git a/src/main/java/org/opentripplanner/routing/location/TemporaryStreetLocation.java b/src/main/java/org/opentripplanner/routing/location/TemporaryStreetLocation.java index 4cacdf91597..d1f894ca93a 100644 --- a/src/main/java/org/opentripplanner/routing/location/TemporaryStreetLocation.java +++ b/src/main/java/org/opentripplanner/routing/location/TemporaryStreetLocation.java @@ -1,6 +1,6 @@ package org.opentripplanner.routing.location; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import org.opentripplanner.routing.edgetype.TemporaryEdge; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.vertextype.TemporaryVertex; diff --git a/src/main/java/org/opentripplanner/routing/services/StreetVertexIndexService.java b/src/main/java/org/opentripplanner/routing/services/StreetVertexIndexService.java index 869c5f492d2..05a6ea75e97 100644 --- a/src/main/java/org/opentripplanner/routing/services/StreetVertexIndexService.java +++ b/src/main/java/org/opentripplanner/routing/services/StreetVertexIndexService.java @@ -1,8 +1,8 @@ package org.opentripplanner.routing.services; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.analyst.core.Sample; import org.opentripplanner.analyst.request.SampleFactory; import org.opentripplanner.common.geometry.GeometryUtils; @@ -291,7 +291,7 @@ public Collection getEdgesForEnvelope(Envelope envelope) { * @param envelope * @return The transit stops within an envelope. */ - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") public List getTransitStopForEnvelope(Envelope envelope) { List transitStops = transitStopTree.query(envelope); for (Iterator its = transitStops.iterator(); its.hasNext();) { diff --git a/src/main/java/org/opentripplanner/routing/spt/GraphPath.java b/src/main/java/org/opentripplanner/routing/spt/GraphPath.java index 9fe606dcb40..bf86a18f3e9 100644 --- a/src/main/java/org/opentripplanner/routing/spt/GraphPath.java +++ b/src/main/java/org/opentripplanner/routing/spt/GraphPath.java @@ -1,17 +1,25 @@ package org.opentripplanner.routing.spt; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.api.resource.CoordinateArrayListSequence; +import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.Trip; import org.opentripplanner.routing.core.RoutingContext; import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.edgetype.flex.TemporaryDirectPatternHop; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.opentripplanner.api.resource.GraphPathToTripPlanConverter.makeCoordinates; + /** * A shortest path on the graph. */ @@ -198,4 +206,43 @@ public RoutingContext getRoutingContext() { return rctx; } + public LineString getGeometry() { + CoordinateArrayListSequence coordinates = makeCoordinates(edges.toArray(new Edge[0])); + return GeometryUtils.getGeometryFactory().createLineString(coordinates); + } + + /** + * Return the total duration, in seconds, of call-and-ride legs used in a trip plan. If no + * call-and-ride legs are used, the duration is 0. + */ + public int getCallAndRideDuration() { + if (states.isEmpty() || !states.getFirst().getOptions().rctx.graph.useFlexService) { + return 0; + } + int duration = 0; + for (State s : states) { + if (s.getBackEdge() != null && s.getBackEdge() instanceof TemporaryDirectPatternHop) { + TemporaryDirectPatternHop hop = (TemporaryDirectPatternHop) s.getBackEdge(); + duration += hop.getDirectVehicleTime(); + } + } + return duration; + } + + /** + * Get all trips used in the search which were call-and-ride trips. Call-and-ride is part of + * GTFS-Flex and must be explicitly turned on in the graph. + */ + public List getCallAndRideTrips() { + if (states.isEmpty() || !states.getFirst().getOptions().rctx.graph.useFlexService) { + return Collections.emptyList(); + } + List trips = new ArrayList<>(); + for (State s : states) { + if (s.getBackEdge() != null && s.getBackEdge() instanceof TemporaryDirectPatternHop) { + trips.add(s.getBackTrip().getId()); + } + } + return trips; + } } diff --git a/src/main/java/org/opentripplanner/routing/spt/SPTWalker.java b/src/main/java/org/opentripplanner/routing/spt/SPTWalker.java index 52b9d57969c..8b6df92f349 100644 --- a/src/main/java/org/opentripplanner/routing/spt/SPTWalker.java +++ b/src/main/java/org/opentripplanner/routing/spt/SPTWalker.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.spt; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.TraverseMode; diff --git a/src/main/java/org/opentripplanner/routing/transit_index/RouteSegment.java b/src/main/java/org/opentripplanner/routing/transit_index/RouteSegment.java index 4fde6a16504..bfc788e85b1 100644 --- a/src/main/java/org/opentripplanner/routing/transit_index/RouteSegment.java +++ b/src/main/java/org/opentripplanner/routing/transit_index/RouteSegment.java @@ -1,8 +1,8 @@ package org.opentripplanner.routing.transit_index; -import com.conveyal.geojson.GeometrySerializer; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.common.geometry.GeometrySerializer; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.routing.graph.Edge; diff --git a/src/main/java/org/opentripplanner/routing/transit_index/RouteVariant.java b/src/main/java/org/opentripplanner/routing/transit_index/RouteVariant.java index 90fc9514461..4b0ca25afe9 100644 --- a/src/main/java/org/opentripplanner/routing/transit_index/RouteVariant.java +++ b/src/main/java/org/opentripplanner/routing/transit_index/RouteVariant.java @@ -28,9 +28,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; /** * diff --git a/src/main/java/org/opentripplanner/routing/trippattern/Deduplicator.java b/src/main/java/org/opentripplanner/routing/trippattern/Deduplicator.java index cd3a070c651..e86f99187b8 100644 --- a/src/main/java/org/opentripplanner/routing/trippattern/Deduplicator.java +++ b/src/main/java/org/opentripplanner/routing/trippattern/Deduplicator.java @@ -15,6 +15,7 @@ public class Deduplicator implements Serializable { private static final long serialVersionUID = 20140524L; private final Map canonicalIntArrays = Maps.newHashMap(); + private final Map canonicalDoubleArrays = Maps.newHashMap(); private final Map canonicalStrings = Maps.newHashMap(); private final Map canonicalBitSets = Maps.newHashMap(); private final Map canonicalStringArrays = Maps.newHashMap(); @@ -22,6 +23,7 @@ public class Deduplicator implements Serializable { /** Free up any memory used by the deduplicator. */ public void reset() { canonicalIntArrays.clear(); + canonicalDoubleArrays.clear(); canonicalStrings.clear(); canonicalBitSets.clear(); canonicalStringArrays.clear(); @@ -39,6 +41,17 @@ public int[] deduplicateIntArray(int[] original) { return canonical.array; } + public double[] deduplicateDoubleArray(double[] original) { + if (original == null) return null; + DoubleArray doubleArray = new DoubleArray(original); + DoubleArray canonical = canonicalDoubleArrays.get(doubleArray); + if (canonical == null) { + canonical = doubleArray; + canonicalDoubleArrays.put(canonical, canonical); + } + return canonical.array; + } + public String deduplicateString(String original) { if (original == null) return null; String canonical = canonicalStrings.get(original); @@ -88,6 +101,25 @@ public int hashCode() { } } + /** A wrapper for a primitive double array. This is insane but necessary in Java. */ + private class DoubleArray implements Serializable { + private static final long serialVersionUID = 20140524L; + final double[] array; + DoubleArray(double[] array) { + this.array = array; + } + @Override + public boolean equals (Object other) { + if (other instanceof DoubleArray) { + return Arrays.equals(array, ((DoubleArray) other).array); + } else return false; + } + @Override + public int hashCode() { + return Arrays.hashCode(array); + } + } + /** A wrapper for a String array. Optionally, the individual Strings may be deduplicated too. */ private class StringArray implements Serializable { private static final long serialVersionUID = 20140524L; diff --git a/src/main/java/org/opentripplanner/routing/trippattern/DrtTravelTime.java b/src/main/java/org/opentripplanner/routing/trippattern/DrtTravelTime.java new file mode 100644 index 00000000000..b27a86f4b72 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/trippattern/DrtTravelTime.java @@ -0,0 +1,85 @@ +package org.opentripplanner.routing.trippattern; + +import java.io.Serializable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DrtTravelTime implements Serializable { + private static final long serialVersionUID = 1L; + + private static final Pattern PATTERN = Pattern.compile( + "(?^[0-9.]+$)|(^(?[0-9.]+)t(\\+(?[0-9.]+))?$)"); + + static final String ERROR_MSG = "Invalid DRT formula. Valid forms are: 3, 4t, 2.5t+5"; + + private String spec; + private double coefficient = 0; + private double constant = 0; + + public static DrtTravelTime fromSpec(String spec) { + Matcher matcher = PATTERN.matcher(spec); + if (matcher.find()) { + // process and convert to seconds + DrtTravelTime tt = new DrtTravelTime(); + String constantStr = matcher.group("onlyConstant"); + if (constantStr != null) { + tt.constant = Double.parseDouble(constantStr) * 60; + } + constantStr = matcher.group("constant"); + if (constantStr != null) { + tt.constant = Double.parseDouble(constantStr) * 60; + } + String coeffStr = matcher.group("coefficient"); + if (coeffStr != null) { + tt.coefficient = Double.parseDouble(coeffStr); + } + tt.spec = spec; + return tt; + } + throw new IllegalArgumentException(ERROR_MSG); + } + + /** + * Given a direct time in seconds, return the time processed by the arithmetic function + * represented by this class. + */ + public double process(double time) { + return ((time * coefficient) + constant); + } + + public double getCoefficient() { + return coefficient; + } + + public double getConstant() { + return constant; + } + + public String getSpec() { + return spec; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DrtTravelTime that = (DrtTravelTime) o; + + if (Double.compare(that.coefficient, coefficient) != 0) return false; + if (Double.compare(that.constant, constant) != 0) return false; + return spec.equals(that.spec); + } + + @Override + public int hashCode() { + int result; + long temp; + result = spec.hashCode(); + temp = Double.doubleToLongBits(coefficient); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(constant); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } +} diff --git a/src/main/java/org/opentripplanner/routing/trippattern/FrequencyEntry.java b/src/main/java/org/opentripplanner/routing/trippattern/FrequencyEntry.java index eaf1e013184..5bdb996e227 100644 --- a/src/main/java/org/opentripplanner/routing/trippattern/FrequencyEntry.java +++ b/src/main/java/org/opentripplanner/routing/trippattern/FrequencyEntry.java @@ -56,11 +56,11 @@ public String toString() { } public int nextDepartureTime (int stop, int time) { - if (time > endTime) return -1; // Start time and end time are for the first stop in the trip. Find the time offset for this stop. int stopOffset = tripTimes.getDepartureTime(stop) - tripTimes.getDepartureTime(0); int beg = startTime + stopOffset; // First time a vehicle passes by this stop. int end = endTime + stopOffset; // Latest a vehicle can pass by this stop. + if (time > end) return -1; if (exactTimes) { for (int dep = beg; dep < end; dep += headway) { if (dep >= time) return dep; @@ -76,14 +76,18 @@ public int nextDepartureTime (int stop, int time) { } public int prevArrivalTime (int stop, int t) { - if (t < startTime) return -1; int stopOffset = tripTimes.getArrivalTime(stop) - tripTimes.getDepartureTime(0); int beg = startTime + stopOffset; // First time a vehicle passes by this stop. int end = endTime + stopOffset; // Latest a vehicle can pass by this stop. + if(t < beg) return -1; if (exactTimes) { - for (int dep = end; dep > beg; dep -= headway) { - if (dep <= t) return dep; + // we can't start from end in case end - beg is not a multiple of headway + int arr; + for (arr = beg + headway; arr < end; arr += headway) { + if (arr > t) return arr - headway; } + // if t > end, return last valid arrival time + return arr - headway; } else { int dep = t - headway; if (dep > end) return end; // not quite right diff --git a/src/main/java/org/opentripplanner/routing/trippattern/TripTimes.java b/src/main/java/org/opentripplanner/routing/trippattern/TripTimes.java index 80dd43c0051..f830ba02c5b 100644 --- a/src/main/java/org/opentripplanner/routing/trippattern/TripTimes.java +++ b/src/main/java/org/opentripplanner/routing/trippattern/TripTimes.java @@ -10,6 +10,7 @@ import org.opentripplanner.common.MavenVersion; import org.opentripplanner.gtfs.BikeAccess; import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.ServiceDay; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.request.BannedStopSet; @@ -95,6 +96,14 @@ public class TripTimes implements Serializable, Comparable, Cloneable */ private final int[] stopSequences; + private final int[] continuousPickup; + + private final int[] continuousDropOff; + + private final double[] serviceAreaRadius; + + private final String[] serviceArea; + /** * The real-time state of this TripTimes. */ @@ -103,6 +112,20 @@ public class TripTimes implements Serializable, Comparable, Cloneable /** A Set of stop indexes that are marked as timepoints in the GTFS input. */ private final BitSet timepoints; + /** + * Demand-Response Transit (DRT) service parameters. In GTFS-Flex, DRT transit is transit which + * is reserved in advance with the provider. This includes call-and-ride service, and + * potentially the deviated portion of deviated-fixed routes. For DRT, the travel time is + * calculated by applying formulas to the direct vehicle time. The following parameters give + * the maximum possible and average travel time formulas, given a direct vehicle time. + */ + + private DrtTravelTime maxTravelTime; + + private DrtTravelTime avgTravelTime; + + private double advanceBookMin = 0; + /** * The provided stopTimes are assumed to be pre-filtered, valid, and monotonically increasing. * The non-interpolated stoptimes should already be marked at timepoints by a previous filtering step. @@ -113,15 +136,50 @@ public TripTimes(final Trip trip, final List stopTimes, final Deduplic final int[] departures = new int[nStops]; final int[] arrivals = new int[nStops]; final int[] sequences = new int[nStops]; + final int[] continuousPickup = new int[nStops]; + final int[] continuousDropOff = new int[nStops]; + final double[] serviceAreaRadius = new double[nStops]; + final String[] serviceArea = new String[nStops]; final BitSet timepoints = new BitSet(nStops); // Times are always shifted to zero. This is essential for frequencies and deduplication. timeShift = stopTimes.get(0).getArrivalTime(); + double radius = 0; + String area = null; int s = 0; for (final StopTime st : stopTimes) { departures[s] = st.getDepartureTime() - timeShift; arrivals[s] = st.getArrivalTime() - timeShift; sequences[s] = st.getStopSequence(); timepoints.set(s, st.getTimepoint() == 1); + continuousPickup[s] = st.getContinuousPickup(); + continuousDropOff[s] = st.getContinuousDropOff(); + + if (st.getStartServiceAreaRadius() != StopTime.MISSING_VALUE) { + radius = st.getStartServiceAreaRadius(); + } + serviceAreaRadius[s] = radius; + if (st.getEndServiceAreaRadius() != StopTime.MISSING_VALUE) { + if (st.getEndServiceAreaRadius() != radius) { + String message = String.format("Trip %s: start service area radius %g does not match end radius %g", + st.getTrip().getId(), radius, st.getEndServiceAreaRadius()); + throw new IllegalArgumentException(message); + } + radius = 0; + } + + if (st.getStartServiceArea() != null) { + area = st.getStartServiceArea().getAreaId(); + } + serviceArea[s] = area; + if (st.getEndServiceArea() != null) { + if (!st.getEndServiceArea().getAreaId().equals(area)) { + String message = String.format("Trip %s: start service area %s does not match end area %s", + st.getTrip().getId(), area, st.getEndServiceArea()); + throw new IllegalArgumentException(message); + } + area = null; + } + s++; } this.scheduledDepartureTimes = deduplicator.deduplicateIntArray(departures); @@ -133,6 +191,17 @@ public TripTimes(final Trip trip, final List stopTimes, final Deduplic this.arrivalTimes = null; this.departureTimes = null; this.timepoints = deduplicator.deduplicateBitSet(timepoints); + this.continuousPickup = deduplicator.deduplicateIntArray(continuousPickup); + this.continuousDropOff = deduplicator.deduplicateIntArray(continuousDropOff); + this.serviceAreaRadius = deduplicator.deduplicateDoubleArray(serviceAreaRadius); + this.serviceArea = deduplicator.deduplicateStringArray(serviceArea); + if (trip.getDrtMaxTravelTime() != null) { + this.maxTravelTime = DrtTravelTime.fromSpec(trip.getDrtMaxTravelTime()); + } + if (trip.getDrtAvgTravelTime() != null) { + this.avgTravelTime = DrtTravelTime.fromSpec(trip.getDrtAvgTravelTime()); + } + this.advanceBookMin = trip.getDrtAdvanceBookMin(); LOG.trace("trip {} has timepoint at indexes {}", trip, timepoints); } @@ -148,6 +217,13 @@ public TripTimes(final TripTimes object) { this.scheduledArrivalTimes = object.scheduledArrivalTimes; this.stopSequences = object.stopSequences; this.timepoints = object.timepoints; + this.continuousPickup = object.continuousPickup; + this.continuousDropOff = object.continuousDropOff; + this.serviceAreaRadius = object.serviceAreaRadius; + this.serviceArea = object.serviceArea; + this.maxTravelTime = object.maxTravelTime; + this.avgTravelTime = object.avgTravelTime; + this.advanceBookMin = object.advanceBookMin; } /** @@ -243,6 +319,56 @@ public int getDepartureDelay(final int stop) { return getDepartureTime(stop) - (scheduledDepartureTimes[stop] + timeShift); } + public int getCallAndRideBoardTime(int stop, long currTime, int directTime, ServiceDay sd, boolean useClockTime, long startClockTime) { + int travelTime = getDemandResponseMaxTime(directTime); + int minBoardTime = getArrivalTime(stop + 1) - travelTime; + int ret = (int) Math.min(Math.max(currTime, getDepartureTime(stop)), minBoardTime); + if (useClockTime) { + int clockTime = (int) (sd.secondsSinceMidnight(startClockTime) + Math.round(trip.getDrtAdvanceBookMin() * 60.0)); + if (ret >= clockTime) { + return ret; + } else if (clockTime < minBoardTime) { + return clockTime; + } else { + return -1; + } + } + return ret; + } + + public int getCallAndRideAlightTime(int stop, long currTime, int directTime, ServiceDay sd, boolean useClockTime, long startClockTime) { + int travelTime = getDemandResponseMaxTime(directTime); + int maxAlightTime = getDepartureTime(stop - 1) + travelTime; + int ret = (int) Math.max(Math.min(currTime, getArrivalTime(stop)), maxAlightTime); + if (useClockTime) { + int clockTime = (int) (sd.secondsSinceMidnight(startClockTime) + Math.round(trip.getDrtAdvanceBookMin() * 60.0)); + // boarding time must be > clockTime + int boardTime = ret - travelTime; + if (boardTime >= clockTime) { + return ret; + } + ret += (clockTime - boardTime); + if (ret >= maxAlightTime) { + return -1; + } + } + return ret; + } + + public int getDemandResponseMaxTime(int directTime) { + if (maxTravelTime != null) { + return (int) Math.round(maxTravelTime.process(directTime)); + } + return directTime; + } + + public int getDemandResponseAvgTime(int directTime) { + if (avgTravelTime != null) { + return (int) Math.round(avgTravelTime.process(directTime)); + } + return directTime; + } + /** * @return true if this TripTimes represents an unmodified, scheduled trip from a published * timetable or false if it is a updated, cancelled, or otherwise modified one. This @@ -272,6 +398,25 @@ public void setRealTimeState(final RealTimeState realTimeState) { this.realTimeState = realTimeState; } + /** Returns whether this stop allows continuous pickup */ + public int getContinuousPickup(final int stop) { + return continuousPickup[stop]; + } + + /** Returns whether this stop allows continuous dropoff */ + public int getContinuousDropOff(final int stop) { + return continuousDropOff[stop]; + } + + /** Returns associated dropoff/pickup radius for this stop*/ + public double getServiceAreaRadius(final int stop) { + return serviceAreaRadius[stop]; + } + + public String getServiceArea(final int stop) { + return serviceArea[stop]; + } + /** Used in debugging / dumping times. */ public static String formatSeconds(int s) { int m = s / 60; diff --git a/src/main/java/org/opentripplanner/routing/util/ElevationUtils.java b/src/main/java/org/opentripplanner/routing/util/ElevationUtils.java index c30e8439c2b..0b5c5de98ea 100644 --- a/src/main/java/org/opentripplanner/routing/util/ElevationUtils.java +++ b/src/main/java/org/opentripplanner/routing/util/ElevationUtils.java @@ -1,8 +1,9 @@ package org.opentripplanner.routing.util; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; import org.opentripplanner.common.geometry.PackedCoordinateSequence; +import org.opentripplanner.routing.util.elevation.ToblersHikingFunction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -167,6 +168,18 @@ public static double getDragResistiveForceComponent(double altitude) { ); } + /** + * If the calculated factor is more than this constant, we ignore the calculated factor and use this + * constant in stead. See ths table in {@link ToblersHikingFunction} for a mapping between the + * factor and angels(degree and percentage). A factor of 3 with take effect for slopes with a + * incline above 31.4% and a decline below 41.4%. The worlds steepest road ia about 35%, and the + * steepest climes in Tour De France is usually in the range 8-12%. Some walking paths may be quite + * steep, but a penalty of 3 is still a large penalty. + */ + private static final double MAX_SLOPE_WALK_EFFECTIVE_LENGTH_FACTOR = 3; + + private static final ToblersHikingFunction toblerWalkingFunction = new ToblersHikingFunction(MAX_SLOPE_WALK_EFFECTIVE_LENGTH_FACTOR); + private static double[] getLengthsFromElevation(CoordinateSequence elev) { double trueLength = 0; @@ -186,7 +199,7 @@ private static double[] getLengthsFromElevation(CoordinateSequence elev) { } /** - * + * * @param elev The elevation profile, where each (x, y) is (distance along edge, elevation) * @param slopeLimit Whether the slope should be limited to 0.35, which is the max slope for * streets that take cars. @@ -199,6 +212,7 @@ public static SlopeCosts getSlopeCosts(CoordinateSequence elev, boolean slopeLim double slopeSpeedEffectiveLength = 0; double slopeWorkCost = 0; double slopeSafetyCost = 0; + double effectiveWalkLength = 0; double[] lengths = getLengthsFromElevation(elev); double trueLength = lengths[0]; double flatLength = lengths[1]; @@ -211,6 +225,7 @@ public static SlopeCosts getSlopeCosts(CoordinateSequence elev, boolean slopeLim 0.0, 1.0, false, + 1.0, new byte[]{0}, new short[]{(short) trueLength}, getDragResistiveForceComponent(0) @@ -277,12 +292,13 @@ public static SlopeCosts getSlopeCosts(CoordinateSequence elev, boolean slopeLim * slope_or_zero * slope_or_zero); slopeWorkCost += energy; double slopeSpeedCoef = slopeSpeedCoefficient(slope, coordinates[i].y); - slopeSpeedEffectiveLength += hypotenuse / slopeSpeedCoef; + slopeSpeedEffectiveLength += run / slopeSpeedCoef; // assume that speed and safety are inverses double safetyCost = hypotenuse * (slopeSpeedCoef - 1) * 0.25; if (safetyCost > 0) { slopeSafetyCost += safetyCost; } + effectiveWalkLength += calculateEffectiveWalkLength(run, rise); } // convert gradient info into arrays of primitives @@ -310,6 +326,7 @@ public static SlopeCosts getSlopeCosts(CoordinateSequence elev, boolean slopeLim maxSlope, lengthMultiplier, flattened, + effectiveWalkLength / flatLength, gradientsArr, gradientLengthsArr, maximumDragResistiveForceComponent @@ -447,54 +464,19 @@ public static double slopeSpeedCoefficient(double slope, double altitude) { } - /** parameter A in the Rees (2004) slope-dependent walk cost model **/ - private static double walkParA = 0.75; - /** parameter C in the Rees (2004) slope-dependent walk cost model **/ - private static double walkParC = 14.6; - /** - * The cost for walking in hilly/mountain terrain dependent on slope using an empirical function by - * WG Rees (Comp & Geosc, 2004), that overhauls the Naismith rule for mountaineering.
- * For a slope of 0 = 0 degree a cost is returned that approximates a speed of 1.333 m/sec = 4.8km/h
- * TODO: Not sure if it makes sense to use maxSlope as input and instead better use - * a lower estimate / average value. However, the DEM is most likely generalized/smoothed - * and hence maxSlope may be smaller than in the real world. - * @param verticalDistance the vertical distance of the line segment - * @param maxSlope the slope of the segment - * @return walk costs dependent on slope (in seconds) + *

+ * We use the Tobler function {@link ToblersHikingFunction} to calculate this. + *

+ *

+ * When testing this we get good results in general, but for some edges + * the elevation profile is not accurate. A (serpentine) road is usually + * build with a constant slope, but the elevation profile in OTP is not + * as smooth, resulting in an extra penalty for these roads. + *

*/ - public static double getWalkCostsForSlope(double verticalDistance, double maxSlope) { - /* - Naismith (1892): - "an hour for every three miles on the map, with an additional hour for - every 2,000 feet of ascent.' - ------- - in S. Fritz and S. Carver (GISRUK 1998): - Naismith's Rule: 5 km/h plus 1 hour per 600m ascent; minus 10 minutes per 300 m - descent for slopes between 5 and 12 degrees; plus 10 minutes per 300m descent - for slopes greater than 12 degrees. - ... - In the case of a 50m grid resolution DEM for every m climbed, 6 seconds are added. - 2 seconds are added in case of a ascent of more than 12 degrees and 2 seconds are - subtracted if the ascent is between 5-12 degrees. - ------- - Naismith's rule was overhauled by W.G. Rees (2004), who developed a quadratic - function for speed estimation: - 1/v = a + b*m + c*m^2 - with a= 0.75 sec/m, b=0.09 s/m, c=14.6 s/m - - As for b=0 there are no big differences the derived cost function is: - k = a*d + c * (h*h) / d - with d= distance, and h = vertical separation - - */ - if (verticalDistance == 0){ - return 0; - } - double costs = 0; - double h = maxSlope * verticalDistance; - costs = (walkParA * verticalDistance) + ( walkParC * (h * h) / verticalDistance); - return costs; + static double calculateEffectiveWalkLength(double run, double rise) { + return run * toblerWalkingFunction.calculateHorizontalWalkingDistanceMultiplier(run, rise); } public static PackedCoordinateSequence getPartialElevationProfile( @@ -524,7 +506,7 @@ public static PackedCoordinateSequence getPartialElevationProfile( //no need to interpolate as this is the first coordinate continue; } - // interpolate start coordinate + // interpolate start coordinate double run = coord.x - lastCoord.x; if (run < 1) { //tiny runs are likely to lead to errors, so we'll skip them @@ -554,7 +536,7 @@ public static PackedCoordinateSequence getPartialElevationProfile( Coordinate coordArr[] = new Coordinate[coordList.size()]; return new PackedCoordinateSequence.Float(coordList.toArray(coordArr), 2); } - + /** checks for units (m/ft) in an OSM ele tag value, and returns the value in meters */ public static Double parseEleTag(String ele) { ele = ele.toLowerCase(); diff --git a/src/main/java/org/opentripplanner/routing/util/SlopeCosts.java b/src/main/java/org/opentripplanner/routing/util/SlopeCosts.java index c1d9edcf82b..f41d8de30b0 100644 --- a/src/main/java/org/opentripplanner/routing/util/SlopeCosts.java +++ b/src/main/java/org/opentripplanner/routing/util/SlopeCosts.java @@ -11,9 +11,17 @@ public class SlopeCosts { public final short[] gradientLengths; // array of the length of each gradient in meters public final double maximumDragResistiveForceComponent; // the maximum resistive drag force component along an edge + /** + * The distance ajusted to incorporate the effect of the slope. Let say the + * distance is 1000 m and 5% uphill, then we can use e.g. the Tobler function + * to calculate the increase of 19% to walk such a distance. We add that + * percentage to the 'flat' distance and get 1190m. + */ + public final double effectiveWalkFactor; + public SlopeCosts(double slopeSpeedFactor, double slopeWorkFactor, double slopeSafetyCost, - double maxSlope, double lengthMultiplier, boolean flattened, byte[] gradients, - short[] gradientLengths, double maximumDragResistiveForceComponent) { + double maxSlope, double lengthMultiplier, boolean flattened, double effectiveWalkFactor, + byte[] gradients, short[] gradientLengths, double maximumDragResistiveForceComponent) { this.slopeSpeedFactor = slopeSpeedFactor; this.slopeWorkFactor = slopeWorkFactor; this.slopeSafetyCost = slopeSafetyCost; @@ -23,5 +31,6 @@ public SlopeCosts(double slopeSpeedFactor, double slopeWorkFactor, double slopeS this.gradients = gradients; this.gradientLengths = gradientLengths; this.maximumDragResistiveForceComponent = maximumDragResistiveForceComponent; + this.effectiveWalkFactor = effectiveWalkFactor; } } diff --git a/src/main/java/org/opentripplanner/routing/util/elevation/ToblersHikingFunction.java b/src/main/java/org/opentripplanner/routing/util/elevation/ToblersHikingFunction.java new file mode 100644 index 00000000000..7193d779b6e --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/util/elevation/ToblersHikingFunction.java @@ -0,0 +1,125 @@ +/* + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package org.opentripplanner.routing.util.elevation; + + +/** + *

+ * Tobler's hiking function is an exponential function determining the hiking speed, taking into account the + * slope angle. It was formulated by Waldo Tobler. This function was estimated from empirical data of Eduard Imhof. + * [ Wikipedia ] + *

+ *
+ * Walking speed(W):
+ *
+ *     W = 6 m/s * Exp(-3.5 * Abs(dh/dx + 0.05))
+ * 
+ * + *

+ * The {@code 6 m/s} is the Maximum speed achieved. This happens at angle 2.86 degrees or -5% downhill. In OTP we + * want to apply this as a multiplier to the horizontal walking distance. This is done for all walkable edges in + * the graph. To find the walkingtime for an edge we use the horizontal walking speed. Therefore: + *

+ *
+ * Given:
+ *   Vflat : Speed at 0 degrees - flat
+ *   Vmax  : Maximum speed (6 m/s)
+ *
+ *   Vmax = C * Vflat
+ *
+ * Then:
+ *               1
+ *   C = ------------------  = 1.19
+ *        EXP(-3.5 * 0.05)
+ * And:
+ *
+ *   dx = Vflat *  t
+ *   W = C * Vflat * EXP(-3.5 * ABS(dh/dx + 0.05))
+ *
+ * The Horizontal walking distance multiplier(F) then becomes:
+ *
+ *                      1
+ *   F = -----------------------------------
+ *        C * EXP(-3.5 * ABS(dh/dx + 0.05))
+ *
+ * Examples:
+ *
+ *   Angle  | Slope % | Horizontal walking distance multiplier
+ *   -------+---------------------------------------
+ *    19,3  |   35 %  |  3,41
+ *    16,7  |   30 %  |  2,86
+ *    14,0  |   25 %  |  2,40
+ *    11,3  |   20 %  |  2,02
+ *     8,5  |   15 %  |  1,69
+ *     5,7  |   10 %  |  1,42
+ *     2,9  |    5 %  |  1,19
+ *     0,0  |    0 %  |  1,00
+ *    −2,9  |   −5 %  |  0,84
+ *    −5,7  |  −10 %  |  1,00
+ *    −8,5  |  −15 %  |  1,19
+ *   −11,3  |  −20 %  |  1,42
+ *   −14,0  |  −25 %  |  1,69
+ *   −16,7  |  −30 %  |  2,02
+ *   −19,3  |  −35 %  |  2,40
+ *   −21,8  |  −40 %  |  2,86
+ *   −24,2  |  −45 %  |  3,41
+ * 
+ */ +public class ToblersHikingFunction { + + /** + * The exponential growth factor in Tobler´s function. + */ + private static final double E = -3.5; + + /** + * The slope offset where the maximum speed will occur. The value 0.05 will result in a maximum speed at + * -2.86 degrees (5% downhill). + */ + private static final double A = 0.05; + + /** The horizontal speed to maximum speed factor: Vmax = C * Vflat */ + private static final double C = 1 / Math.exp(E * A); + + + private final double walkDistMultiplierMaxLimit; + + + /** + * @param walkDistMultiplierMaxLimit this property is used to set a maximum limit for the horizontal walking + * distance multiplier. Must be > 1.0. See the table in the class documentation + * for finding reasonable values for this constant. + */ + public ToblersHikingFunction(double walkDistMultiplierMaxLimit) { + if(walkDistMultiplierMaxLimit < 1.0) { + throw new IllegalArgumentException("The 'walkDistMultiplierMaxLimit' is " + walkDistMultiplierMaxLimit + + ", but must be greater then 1."); + } + this.walkDistMultiplierMaxLimit = walkDistMultiplierMaxLimit; + } + + /** + * Calculate a walking distance multiplier to account tor the slope penalty. + * @param dx The horizontal walking distance + * @param dh The vertical distance (height) + */ + public double calculateHorizontalWalkingDistanceMultiplier(double dx, double dh) { + + double distanceMultiplier = 1.0 / (C * Math.exp(E * Math.abs(dh/dx + A))); + + return distanceMultiplier < walkDistMultiplierMaxLimit ? distanceMultiplier : walkDistMultiplierMaxLimit; + } +} diff --git a/src/main/java/org/opentripplanner/routing/vehicle_rental/VehicleRentalRegion.java b/src/main/java/org/opentripplanner/routing/vehicle_rental/VehicleRentalRegion.java index 08fa39d0b50..23235e2abdd 100644 --- a/src/main/java/org/opentripplanner/routing/vehicle_rental/VehicleRentalRegion.java +++ b/src/main/java/org/opentripplanner/routing/vehicle_rental/VehicleRentalRegion.java @@ -14,7 +14,7 @@ the License, or (at your option) any later version. package org.opentripplanner.routing.vehicle_rental; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; import javax.xml.bind.annotation.XmlAttribute; import java.io.Serializable; diff --git a/src/main/java/org/opentripplanner/routing/vertextype/PatternArriveVertex.java b/src/main/java/org/opentripplanner/routing/vertextype/PatternArriveVertex.java index e4dcf0f2805..4f841c92d3f 100644 --- a/src/main/java/org/opentripplanner/routing/vertextype/PatternArriveVertex.java +++ b/src/main/java/org/opentripplanner/routing/vertextype/PatternArriveVertex.java @@ -1,8 +1,11 @@ package org.opentripplanner.routing.vertextype; +import org.opentripplanner.model.Stop; import org.opentripplanner.routing.edgetype.TripPattern; import org.opentripplanner.routing.graph.Graph; +import java.util.Random; + public class PatternArriveVertex extends PatternStopVertex { private static final long serialVersionUID = 20140101; @@ -12,6 +15,11 @@ public PatternArriveVertex(Graph g, TripPattern pattern, int stopIndex) { super(g, makeLabel(pattern, stopIndex), pattern, pattern.stopPattern.stops[stopIndex]); } + /** constructor for temporary trip patterns */ + public PatternArriveVertex(Graph g, TripPattern pattern, int stopIndex, Stop stop) { + super(g, makeTemporaryLabel(pattern, stopIndex), pattern, stop); + } + // constructor for frequency patterns is now missing // it is possible to have both a freq and non-freq pattern with the same stop pattern @@ -19,5 +27,8 @@ private static String makeLabel(TripPattern pattern, int stop) { return String.format("%s_%02d_A", pattern.code, stop); } + private static String makeTemporaryLabel(TripPattern pattern, int stop) { + return String.format("%s_%02d_A_%d", pattern.code + "_temp", stop, new Random().nextInt()); + } } diff --git a/src/main/java/org/opentripplanner/routing/vertextype/PatternDepartVertex.java b/src/main/java/org/opentripplanner/routing/vertextype/PatternDepartVertex.java index 03cf21317df..2ea7428f006 100644 --- a/src/main/java/org/opentripplanner/routing/vertextype/PatternDepartVertex.java +++ b/src/main/java/org/opentripplanner/routing/vertextype/PatternDepartVertex.java @@ -1,8 +1,11 @@ package org.opentripplanner.routing.vertextype; +import org.opentripplanner.model.Stop; import org.opentripplanner.routing.edgetype.TripPattern; import org.opentripplanner.routing.graph.Graph; +import java.util.Random; + public class PatternDepartVertex extends PatternStopVertex { private static final long serialVersionUID = 20140101; @@ -12,6 +15,11 @@ public PatternDepartVertex(Graph g, TripPattern pattern, int stopIndex) { super(g, makeLabel(pattern, stopIndex), pattern, pattern.stopPattern.stops[stopIndex]); } + /** constructor for temporary trip patterns */ + public PatternDepartVertex(Graph g, TripPattern pattern, int stopIndex, Stop stop) { + super(g, makeTemporaryLabel(pattern, stopIndex), pattern, stop); + } + // constructor for single-trip hops with no trip pattern (frequency patterns) is now missing // it is possible to have both a freq and non-freq pattern with the same stop pattern @@ -19,5 +27,8 @@ private static String makeLabel(TripPattern pattern, int stop) { return String.format("%s_%02d_D", pattern.code, stop); } - + private static String makeTemporaryLabel(TripPattern pattern, int stop) { + return String.format("%s_%02d_D_%d", pattern.code + "_temp", stop, new Random().nextInt()); + } + } diff --git a/src/main/java/org/opentripplanner/routing/vertextype/SampleVertex.java b/src/main/java/org/opentripplanner/routing/vertextype/SampleVertex.java index d56dfa8e8f0..10b3ed6a8f9 100644 --- a/src/main/java/org/opentripplanner/routing/vertextype/SampleVertex.java +++ b/src/main/java/org/opentripplanner/routing/vertextype/SampleVertex.java @@ -1,6 +1,6 @@ package org.opentripplanner.routing.vertextype; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import org.opentripplanner.routing.edgetype.TemporaryEdge; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; diff --git a/src/main/java/org/opentripplanner/routing/vertextype/StreetVertex.java b/src/main/java/org/opentripplanner/routing/vertextype/StreetVertex.java index 99a7fd3e615..64b3c78cf2a 100644 --- a/src/main/java/org/opentripplanner/routing/vertextype/StreetVertex.java +++ b/src/main/java/org/opentripplanner/routing/vertextype/StreetVertex.java @@ -5,7 +5,7 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import org.opentripplanner.util.I18NString; import org.opentripplanner.util.LocalizedString; import org.opentripplanner.util.NonLocalizedString; diff --git a/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertex.java b/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertex.java index 438937c1b64..950afca4788 100644 --- a/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertex.java +++ b/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertex.java @@ -2,6 +2,8 @@ import org.opentripplanner.routing.graph.Vertex; +import java.util.Collection; + /** * Marker interface for temporary vertices. *

@@ -12,15 +14,42 @@ public interface TemporaryVertex { boolean isEndVertex(); /** - * This method traverse the subgraph of temporary vertices, and cuts that subgraph off from the - * main graph at each point it encounters a non-temporary vertexes. OTP then holds no + * This method traverses the subgraph of temporary vertices, and cuts that subgraph off from + * the main graph at each point it encounters a non-temporary vertexes. OTP then holds no * references to the temporary subgraph and it is garbage collected. *

* Note! If the {@code vertex} is NOT a TemporaryVertex the method returns. No action taken. * * @param vertex Vertex part of the temporary part of the graph. + * @return a collection of all the vertices removed from the graph. + */ + static Collection dispose(Vertex vertex) { + return TemporaryVertexDispose.dispose(vertex); + } + + /** + * This method traverses the subgraph of all temporary vertices connected to the collection, + * and removes connections to the main graph. OTP then holds no references to any of the + * temporary subgraphs and they can be garbage-collected. + *

+ * Note! Any vertices which are not instances of TemporaryVertex are ignored and no action is + * taken. + * + * @param vertices All temporary vertices + * @return a collection of all the vertices removed from the graph. + */ + static Collection disposeAll(Collection vertices) { + return TemporaryVertexDispose.disposeAll(vertices); + } + + /** + * Return the subgraph of temporary vertices which are connected to this vertex. Returns an + * empty collection if the given vertex is not a TemporaryVertex. + * + * @param vertex Vertex part of the temporary vertex subgraph. + * @return a collection of all temporary vertices in the subgraph. */ - static void dispose(Vertex vertex) { - TemporaryVertexDispose.dispose(vertex); + static Collection findSubgraph(Vertex vertex) { + return TemporaryVertexDispose.search(vertex); } } diff --git a/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertexDispose.java b/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertexDispose.java index 9fa8976a352..7c3d1d60ab5 100644 --- a/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertexDispose.java +++ b/src/main/java/org/opentripplanner/routing/vertextype/TemporaryVertexDispose.java @@ -4,6 +4,8 @@ import org.opentripplanner.routing.graph.Vertex; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -21,7 +23,7 @@ */ class TemporaryVertexDispose { /** - * A list of all Vertexes not jet processed. + * A list of all Vertexes not yet processed. */ private List todo = new ArrayList<>(); @@ -36,20 +38,55 @@ private TemporaryVertexDispose(Vertex tempVertex) { todo.add(tempVertex); } + private TemporaryVertexDispose() { + } + /** * Create an instance and dispose temporary subgraph. - * @param tempVertex any temporary vertex part of the temporary subgrap. + * @param tempVertex any temporary vertex part of the temporary subgraph. + * @return a collection of all the vertices removed from the graph. */ - static void dispose(Vertex tempVertex) { - if(tempVertex instanceof TemporaryVertex) { - new TemporaryVertexDispose(tempVertex).dispose(); + static Collection dispose(Vertex tempVertex) { + return search(tempVertex, true); + } + + /** + * Create an instance and dispose temporary subgraphs. + * @param vertices all temporary vertices to search from to dispose subgraphs + * @return a collection of all vertices removed from the graph + */ + static Collection disposeAll(Collection vertices) { + TemporaryVertexDispose task = new TemporaryVertexDispose(); + for (Vertex vertex : vertices) { + if (vertex instanceof TemporaryVertex) { + task.todo.add(vertex); + } } + task.search(true); + return task.done; } + /** + * Create an instance and discover TemporaryVertex subgraph. + * @param tempVertex any temporary vertex part of the temporary subgraph. + * @return a collection of all the vertices connected to this vertex. + */ + static Collection search(Vertex tempVertex) { + return search(tempVertex, false); + } /* private methods */ - private void dispose() { + private static Collection search(Vertex tempVertex, boolean dispose) { + if(tempVertex instanceof TemporaryVertex) { + TemporaryVertexDispose task = new TemporaryVertexDispose(tempVertex); + task.search(dispose); + return task.done; + } + return Collections.emptySet(); + } + + private void search(boolean dispose) { // Add all connected vertexes to the TODO_list and disconnect all // main graph vertexes. We use a loop and not recursion to avoid // stack overflow in the case of deep temporary graphs. @@ -57,10 +94,10 @@ private void dispose() { Vertex current = next(); if(isNotAlreadyProcessed(current)) { for (Edge edge : current.getOutgoing()) { - disposeVertex(edge.getToVertex(), edge, true); + visitVertex(edge.getToVertex(), edge, true, dispose); } for (Edge edge : current.getIncoming()) { - disposeVertex(edge.getFromVertex(), edge, false); + visitVertex(edge.getFromVertex(), edge, false, dispose); } done.add(current); } @@ -68,18 +105,19 @@ private void dispose() { } /** - * Add the temporary vertex to processing queue OR disconnect edge from vertex if - * vertex is part of the main graph. + * Add the temporary vertex to processing queue, or if dispose = true, disconnect edge from + * vertex if vertex is part of the main graph. * * @param v the vertex to dispose * @param connectedEdge the connected temporary edge * @param incoming true if the edge is an incoming edge, false if it is an outgoing edge + * @param dispose true if edge should be disconnected from the vertex */ - private void disposeVertex(Vertex v, Edge connectedEdge, boolean incoming) { + private void visitVertex(Vertex v, Edge connectedEdge, boolean incoming, boolean dispose) { if(v instanceof TemporaryVertex) { addVertexToProcessTodoList(v); } - else { + else if (dispose) { removeEdgeFromMainGraphVertex(v, connectedEdge, incoming); } } diff --git a/src/main/java/org/opentripplanner/routing/vertextype/TransitStop.java b/src/main/java/org/opentripplanner/routing/vertextype/TransitStop.java index 4b868435367..6dc892725d7 100644 --- a/src/main/java/org/opentripplanner/routing/vertextype/TransitStop.java +++ b/src/main/java/org/opentripplanner/routing/vertextype/TransitStop.java @@ -1,11 +1,14 @@ package org.opentripplanner.routing.vertextype; import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.core.RoutingContext; +import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.core.TraverseModeSet; import org.opentripplanner.routing.edgetype.PathwayEdge; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.vertextype.flex.TemporaryTransitStop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +46,10 @@ public TransitStop(Graph graph, Stop stop) { this.wheelchairEntrance = stop.getWheelchairBoarding() != 2; isEntrance = stop.getLocationType() == 2; //Adds this vertex into graph envelope so that we don't need to loop over all vertices - graph.expandToInclude(stop.getLon(), stop.getLat()); + // Temporary transit stops, needed for GTFS-Flex support, will not be added to the envelope. + if (graph != null) { + graph.expandToInclude(stop.getLon(), stop.getLat()); + } } public boolean hasWheelchairEntrance() { @@ -88,4 +94,24 @@ public void addMode(TraverseMode mode) { public boolean isStreetLinkable() { return isEntrance() || !hasEntrances(); } + + // We want to avoid a situation where results look like + // 1) call-and-ride from A to B + // 2) call-and-ride from A to real transit stop right next to B, walk to B + public boolean checkCallAndRideBoardAlightOk(State s0) { + RoutingContext rctx = s0.getOptions().rctx; + if (this == rctx.fromVertex || this == rctx.toVertex) { + return true; + } + if (!s0.isEverBoarded()) { + return false; + } + // only allow call-n-ride transfers at the same stop + return s0.getPreviousStop().equals(getStop()); + } + + public boolean checkCallAndRideStreetLinkOk(State s0) { + RoutingContext rctx = s0.getOptions().rctx; + return this == rctx.fromVertex || this == rctx.toVertex; + } } diff --git a/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryPatternArriveVertex.java b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryPatternArriveVertex.java new file mode 100644 index 00000000000..deadfc04222 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryPatternArriveVertex.java @@ -0,0 +1,18 @@ +package org.opentripplanner.routing.vertextype.flex; + +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.edgetype.TripPattern; +import org.opentripplanner.routing.vertextype.PatternArriveVertex; +import org.opentripplanner.routing.vertextype.TemporaryVertex; + +public class TemporaryPatternArriveVertex extends PatternArriveVertex implements TemporaryVertex { + + public TemporaryPatternArriveVertex(TripPattern pattern, int stopIndex, Stop stop) { + super(null, pattern, stopIndex, stop); + } + + @Override + public boolean isEndVertex() { + return false; + } +} diff --git a/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryPatternDepartVertex.java b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryPatternDepartVertex.java new file mode 100644 index 00000000000..550eba9d24d --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryPatternDepartVertex.java @@ -0,0 +1,18 @@ +package org.opentripplanner.routing.vertextype.flex; + +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.edgetype.TripPattern; +import org.opentripplanner.routing.vertextype.PatternDepartVertex; +import org.opentripplanner.routing.vertextype.TemporaryVertex; + +public class TemporaryPatternDepartVertex extends PatternDepartVertex implements TemporaryVertex { + + public TemporaryPatternDepartVertex(TripPattern pattern, int stopIndex, Stop stop) { + super(null, pattern, stopIndex, stop); + } + + @Override + public boolean isEndVertex() { + return false; + } +} diff --git a/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStop.java b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStop.java new file mode 100644 index 00000000000..6a806afbcde --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStop.java @@ -0,0 +1,37 @@ +package org.opentripplanner.routing.vertextype.flex; + +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.vertextype.StreetVertex; +import org.opentripplanner.routing.vertextype.TemporaryVertex; +import org.opentripplanner.routing.vertextype.TransitStop; + +public class TemporaryTransitStop extends TransitStop implements TemporaryVertex { + + // stop is *at* a street vertex + private StreetVertex streetVertex; + + public TemporaryTransitStop(Stop stop, StreetVertex streetVertex) { + super(null, stop); + this.streetVertex = streetVertex; + } + + @Override + public boolean isEndVertex() { + return false; + } + + public StreetVertex getStreetVertex() { + return streetVertex; + } + + @Override + public boolean checkCallAndRideBoardAlightOk(State state) { + return true; + } + + @Override + public boolean checkCallAndRideStreetLinkOk(State s0) { + return true; + } +} diff --git a/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStopArrive.java b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStopArrive.java new file mode 100644 index 00000000000..0c2dab4fe0e --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStopArrive.java @@ -0,0 +1,17 @@ +package org.opentripplanner.routing.vertextype.flex; + +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.vertextype.TemporaryVertex; +import org.opentripplanner.routing.vertextype.TransitStop; +import org.opentripplanner.routing.vertextype.TransitStopArrive; + +public class TemporaryTransitStopArrive extends TransitStopArrive implements TemporaryVertex { + public TemporaryTransitStopArrive(Stop stop, TransitStop transitStop) { + super(null, stop, transitStop); + } + + @Override + public boolean isEndVertex() { + return false; + } +} diff --git a/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStopDepart.java b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStopDepart.java new file mode 100644 index 00000000000..14eb6a96801 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/vertextype/flex/TemporaryTransitStopDepart.java @@ -0,0 +1,17 @@ +package org.opentripplanner.routing.vertextype.flex; + +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.vertextype.TemporaryVertex; +import org.opentripplanner.routing.vertextype.TransitStop; +import org.opentripplanner.routing.vertextype.TransitStopDepart; + +public class TemporaryTransitStopDepart extends TransitStopDepart implements TemporaryVertex { + public TemporaryTransitStopDepart(Stop stop, TransitStop transitStop) { + super(null, stop, transitStop); + } + + @Override + public boolean isEndVertex() { + return false; + } +} diff --git a/src/main/java/org/opentripplanner/scripting/api/OtpsIndividual.java b/src/main/java/org/opentripplanner/scripting/api/OtpsIndividual.java index 891c187a1ca..b5525ee7ba2 100644 --- a/src/main/java/org/opentripplanner/scripting/api/OtpsIndividual.java +++ b/src/main/java/org/opentripplanner/scripting/api/OtpsIndividual.java @@ -6,9 +6,9 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.spt.ShortestPathTree; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.linearref.LengthIndexedLine; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.linearref.LengthIndexedLine; /** * An individual is a point with coordinates, associated with some optional values (string, floats, diff --git a/src/main/java/org/opentripplanner/standalone/GraphBuilderParameters.java b/src/main/java/org/opentripplanner/standalone/GraphBuilderParameters.java index 64f112500e4..936c6cb62da 100644 --- a/src/main/java/org/opentripplanner/standalone/GraphBuilderParameters.java +++ b/src/main/java/org/opentripplanner/standalone/GraphBuilderParameters.java @@ -104,6 +104,13 @@ public class GraphBuilderParameters { /** If specified, download NED elevation tiles from the given AWS S3 bucket. */ public final S3BucketConfig elevationBucket; + /** + * Unit conversion multiplier for elevation values. No conversion needed if the elevation values + * are defined in meters in the source data. If, for example, decimetres are used in the source data, + * this should be set to 0.1. + */ + public final double elevationUnitMultiplier; + /** * A specific fares service to use. */ @@ -229,6 +236,7 @@ public GraphBuilderParameters(JsonNode config) { platformEntriesLinking = config.path("platformEntriesLinking").asBoolean(false); fetchElevationUS = config.path("fetchElevationUS").asBoolean(false); elevationBucket = S3BucketConfig.fromConfig(config.path("elevationBucket")); + elevationUnitMultiplier = config.path("elevationUnitMultiplier").asDouble(1); fareServiceFactory = DefaultFareServiceFactory.fromConfig(config.path("fares")); customNamer = CustomNamer.CustomNamerFactory.fromConfig(config.path("osmNaming")); wayPropertySet = WayPropertySetSource.fromConfig(config.path("osmWayPropertySet").asText("default")); diff --git a/src/main/java/org/opentripplanner/standalone/OTPMain.java b/src/main/java/org/opentripplanner/standalone/OTPMain.java index 5c8fede3531..50d768c3ac9 100644 --- a/src/main/java/org/opentripplanner/standalone/OTPMain.java +++ b/src/main/java/org/opentripplanner/standalone/OTPMain.java @@ -69,8 +69,9 @@ public static void main(String[] args) { } OTPMain main = new OTPMain(params); - main.run(); - + if (!main.run()) { + System.exit(-1); + } } /* Constructor. */ @@ -81,8 +82,12 @@ public OTPMain(CommandLineParameters params) { /** * Making OTPMain a concrete class and placing this logic an instance method instead of embedding it in the static * main method makes it possible to build graphs from web services or scripts, not just from the command line. + * + * @return + * true - if the OTPServer starts successfully. If "Run an OTP API server" has been requested, this method will return when the web server shuts down; + * false - if an error occurs while loading the graph; */ - public void run() { + public boolean run() { // TODO do params.infer() here to ensure coherency? @@ -106,8 +111,8 @@ public void run() { graphService.registerGraph("", new MemoryGraphSource("", graph)); } } else { - LOG.error("An error occurred while building the graph. Exiting."); - System.exit(-1); + LOG.error("An error occurred while building the graph."); + return false; } } @@ -153,7 +158,7 @@ public void run() { while (true) { // Loop to restart server on uncaught fatal exceptions. try { grizzlyServer.run(); - return; + return true; } catch (Throwable throwable) { LOG.error("An uncaught {} occurred inside OTP. Restarting server.", throwable.getClass().getSimpleName(), throwable); @@ -161,6 +166,7 @@ public void run() { } } + return true; } /** diff --git a/src/main/java/org/opentripplanner/standalone/Router.java b/src/main/java/org/opentripplanner/standalone/Router.java index 6b0d00033d5..5a3ac869e64 100644 --- a/src/main/java/org/opentripplanner/standalone/Router.java +++ b/src/main/java/org/opentripplanner/standalone/Router.java @@ -154,6 +154,12 @@ public void startup(JsonNode config) { } } + /* Set whether to use flex service */ + JsonNode useFlexService = config.get("useFlexService"); + if (useFlexService != null) { + graph.setUseFlexService(useFlexService.asBoolean(false)); + } + /* Create Graph updater modules from JSON config. */ GraphUpdaterConfigurator.setupGraph(this.graph, config); diff --git a/src/main/java/org/opentripplanner/traffic/Segment.java b/src/main/java/org/opentripplanner/traffic/Segment.java deleted file mode 100644 index 09b1d27f0eb..00000000000 --- a/src/main/java/org/opentripplanner/traffic/Segment.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.opentripplanner.traffic; - -import io.opentraffic.engine.data.pbf.ExchangeFormat; -import org.opentripplanner.routing.edgetype.StreetEdge; - -import java.io.Serializable; - -/** - * Represents a segment of an OSM way. - */ -public class Segment implements Serializable { - /** - * ID of the way to which this is attached - */ - public final long wayId; - - /** - * ID of the starting OSM node. - */ - public final long startNodeId; - - /** - * ID of the ending OSM node. - */ - public final long endNodeId; - - public Segment (ExchangeFormat.SegmentDefinition segmentDefinition) { - this.wayId = segmentDefinition.getWayId(); - this.startNodeId = segmentDefinition.getStartNodeId(); - this.endNodeId = segmentDefinition.getEndNodeId(); - } - - public Segment(StreetEdge edge) { - this.wayId = edge.wayId; - this.endNodeId = edge.getEndOsmNodeId(); - this.startNodeId = edge.getStartOsmNodeId(); - } - - public Segment (long wayId, long startNodeId, long endNodeId) { - this.wayId = wayId; - this.startNodeId = startNodeId; - this.endNodeId = endNodeId; - } - - // this is used as a hashmap key so needs to have semantic equality and hash - @Override public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - Segment segment = (Segment) o; - - if (endNodeId != segment.endNodeId) - return false; - if (startNodeId != segment.startNodeId) - return false; - if (wayId != segment.wayId) - return false; - - return true; - } - - @Override public int hashCode() { - int result = (int) (wayId ^ (wayId >>> 32)); - result = 31 * result + (int) (startNodeId ^ (startNodeId >>> 32)); - result = 31 * result + (int) (endNodeId ^ (endNodeId >>> 32)); - return result; - } -} diff --git a/src/main/java/org/opentripplanner/traffic/SegmentSpeedSample.java b/src/main/java/org/opentripplanner/traffic/SegmentSpeedSample.java deleted file mode 100644 index af53a698f49..00000000000 --- a/src/main/java/org/opentripplanner/traffic/SegmentSpeedSample.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.opentripplanner.traffic; - -import io.opentraffic.engine.data.pbf.ExchangeFormat; -import io.opentraffic.engine.data.stats.SummaryStatistics; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Serializable; -import java.time.DayOfWeek; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; - -/** - * Represents speeds at particular times of day. - */ -public class SegmentSpeedSample implements Serializable { - private static final Logger LOG = LoggerFactory.getLogger(SegmentSpeedSample.class); - - private static final double KMH_TO_MS = 1000d / 3600d; - - /** - * the overall average speed on this segment, in centimeters per second, with -32,768 representing 0. - * This allows representation of speeds up to 2359 kilometers per hour. - */ - private final short average; - - /** - * The average speeds by hour of week, with 0 being midnight Monday morning GMT. - * Coded as above. - */ - private final short[] hourBins; - - /** Get a speed estimate in meters per second for the time specified (in milliseconds since the epoch) */ - public double getSpeed (long time) { - if (hourBins == null) - return decodeSpeed(average); - - // figure out the hour bin - Instant instant = Instant.ofEpochMilli(time); - - OffsetDateTime dt = instant.atOffset(ZoneOffset.UTC); - - // 0 (Monday) to 6 (Sunday) after subtraction - int day = DayOfWeek.from(dt).getValue() - 1; - int hour = dt.getHour(); - - int hourBin = day * 24 + hour; - return decodeSpeed(hourBins[hourBin]); - } - - /** Decode a speed to meters per second from its short representation */ - private double decodeSpeed (short speed) { - return (((double) speed) - Short.MIN_VALUE) / 100d; - } - - /** Encode a speed stored as meters per second to its short representation. */ - private short encodeSpeed (double speed) { - if (speed < 0) - throw new UnsupportedOperationException("negative speeds do not exist."); - - if (speed > 65535 / 100d) { - LOG.warn("Speed is greater than 2359.26 kilometers per hour, clamping. However, are you certain that there is a road with a speed this fast?"); - return Short.MAX_VALUE; - } - - - return (short) (speed * 100 - Short.MIN_VALUE); - } - - /** Create a speed sample from an OpenTraffic PBF stats object */ - public SegmentSpeedSample(ExchangeFormat.BaselineStats stats) { - float avg = stats.getAverageSpeed(); - - if (Float.isNaN(avg)) { - LOG.error("Invalid speed sample: average speed is NaN"); - throw new IllegalArgumentException("Overall average speed for a sample is NaN."); - } - - this.average = encodeSpeed(avg * KMH_TO_MS); - - int count = stats.getHourOfWeekAveragesCount(); - - if (count == 7 * 24) { - hourBins = new short[count]; - - for (int i = 0; i < count; i++) { - float speed = stats.getHourOfWeekAverages(i); - - if (!Float.isNaN(speed)) - hourBins[i] = encodeSpeed(speed * KMH_TO_MS); - else - hourBins[i] = average; - } - } - else { - if (count > 0 ) - LOG.error("Expected {} hours in speed sample, found {}", 7 * 24, count); - - hourBins = null; - } - } - - /** Create a speed sample from an OpenTraffic stats object directly */ - public SegmentSpeedSample(SummaryStatistics stats) { - double avg = stats.getMean(); - - if (Double.isNaN(avg)) { - LOG.error("Invalid speed sample: average speed is NaN"); - throw new IllegalArgumentException("Overall average speed for a sample is NaN."); - } - - this.average = encodeSpeed(avg); - - hourBins = new short[7 * 24]; - - for (int i = 0; i < 7 * 24; i++) { - double speed = stats.getMean(); //TODO make it possible to grab summary by hour - - if (!Double.isNaN(speed)) - hourBins[i] = encodeSpeed(speed); - else - hourBins[i] = average; - } - - } - - /** create a speed sample using a function */ - public SegmentSpeedSample(double averageSpeed, double[] hourBins) { - this.average = encodeSpeed(averageSpeed); - this.hourBins = new short[hourBins.length]; - - for (int i = 0; i < hourBins.length; i++) { - this.hourBins[i] = Double.isNaN(hourBins[i]) ? this.average : encodeSpeed(hourBins[i]); - } - } -} diff --git a/src/main/java/org/opentripplanner/traffic/StreetSpeedSnapshot.java b/src/main/java/org/opentripplanner/traffic/StreetSpeedSnapshot.java deleted file mode 100644 index 2edd8a8a211..00000000000 --- a/src/main/java/org/opentripplanner/traffic/StreetSpeedSnapshot.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.opentripplanner.traffic; - -import org.opentripplanner.routing.core.TraverseMode; -import org.opentripplanner.routing.edgetype.StreetEdge; - -import java.util.Map; - -/** - * A source of speeds for traversing streets. - */ -public class StreetSpeedSnapshot { - private final Map samples; - - /** Get the speed for traversing the given edge with the given mode at the given time. Returns NaN if there is no speed information available. */ - public double getSpeed (StreetEdge edge, TraverseMode traverseMode, long timeMillis) { - if (traverseMode != TraverseMode.CAR) - return Double.NaN; - - SegmentSpeedSample sample = samples.get(new Segment(edge)); - - if (sample == null) return Double.NaN; - - return sample.getSpeed(timeMillis); - } - - public StreetSpeedSnapshot (Map samples) { - this.samples = samples; - } -} diff --git a/src/main/java/org/opentripplanner/traffic/StreetSpeedSnapshotSource.java b/src/main/java/org/opentripplanner/traffic/StreetSpeedSnapshotSource.java deleted file mode 100644 index 7c1123e5869..00000000000 --- a/src/main/java/org/opentripplanner/traffic/StreetSpeedSnapshotSource.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.opentripplanner.traffic; - -/** - * Keeps track of street speed snapshots, handles concurrency. - */ -public class StreetSpeedSnapshotSource { - private StreetSpeedSnapshot snapshot; - - /** Get a speed snapshot. */ - // not synchronized; reference writes and reads are atomic in java - public StreetSpeedSnapshot getSnapshot () { - return this.snapshot; - } - - public synchronized void setSnapshot(StreetSpeedSnapshot snapshot) { - this.snapshot = snapshot; - } -} diff --git a/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java b/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java index 56571eabd29..a115940fc40 100644 --- a/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java +++ b/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java @@ -12,7 +12,6 @@ import org.opentripplanner.updater.stoptime.PollingStoptimeUpdater; import org.opentripplanner.updater.stoptime.WebsocketGtfsRealtimeUpdater; import org.opentripplanner.updater.street_notes.WinkkiPollingGraphUpdater; -import org.opentripplanner.updater.traffic.OpenTrafficUpdater; import org.opentripplanner.updater.transportation_network_company.TransportationNetworkCompanyUpdater; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,9 +94,6 @@ else if (type.equals("example-polling-updater")) { else if (type.equals("winkki-polling-updater")) { updater = new WinkkiPollingGraphUpdater(); } - else if (type.equals("opentraffic-updater")) { - updater = new OpenTrafficUpdater(); - } else if (type.equals("transportation-network-company-updater")) { updater = new TransportationNetworkCompanyUpdater(); } diff --git a/src/main/java/org/opentripplanner/updater/alerts/GtfsRealtimeAlertsUpdater.java b/src/main/java/org/opentripplanner/updater/alerts/GtfsRealtimeAlertsUpdater.java index e5cd89d2d35..42efec5d48c 100644 --- a/src/main/java/org/opentripplanner/updater/alerts/GtfsRealtimeAlertsUpdater.java +++ b/src/main/java/org/opentripplanner/updater/alerts/GtfsRealtimeAlertsUpdater.java @@ -85,7 +85,10 @@ public void setup(Graph graph) { @Override protected void runPolling() { try { - InputStream data = HttpUtils.getData(url); + InputStream data = HttpUtils.getData( + url, + "Accept", + "application/x-google-protobuf, application/x-protobuf, application/protobuf, application/octet-stream, */*"); if (data == null) { throw new RuntimeException("Failed to get data from url " + url); } diff --git a/src/main/java/org/opentripplanner/updater/car_rental/CarRentalUpdater.java b/src/main/java/org/opentripplanner/updater/car_rental/CarRentalUpdater.java index a99482d2da7..e0e0be1d162 100644 --- a/src/main/java/org/opentripplanner/updater/car_rental/CarRentalUpdater.java +++ b/src/main/java/org/opentripplanner/updater/car_rental/CarRentalUpdater.java @@ -1,13 +1,13 @@ package org.opentripplanner.updater.car_rental; import com.fasterxml.jackson.databind.JsonNode; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.prep.PreparedGeometry; -import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; import org.opentripplanner.graph_builder.linking.StreetSplitter; import org.opentripplanner.routing.car_rental.CarRentalRegion; import org.opentripplanner.routing.car_rental.CarRentalStation; diff --git a/src/main/java/org/opentripplanner/updater/car_rental/GenericCarRentalDataSource.java b/src/main/java/org/opentripplanner/updater/car_rental/GenericCarRentalDataSource.java index 1d4403b4d53..a152c9bd8e4 100644 --- a/src/main/java/org/opentripplanner/updater/car_rental/GenericCarRentalDataSource.java +++ b/src/main/java/org/opentripplanner/updater/car_rental/GenericCarRentalDataSource.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryCollection; -import com.vividsolutions.jts.geom.GeometryFactory; import org.geojson.Feature; import org.geojson.FeatureCollection; import org.geojson.GeoJsonObject; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; import org.opentripplanner.analyst.UnsupportedGeometryException; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.routing.car_rental.CarRentalRegion; diff --git a/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java b/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java index 56887181891..d5a2b00dfa2 100644 --- a/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java +++ b/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java @@ -51,7 +51,10 @@ public List getUpdates() { List updates = null; fullDataset = true; try { - InputStream is = HttpUtils.getData(url); + InputStream is = HttpUtils.getData( + url, + "Accept", + "application/x-google-protobuf, application/x-protobuf, application/protobuf, application/octet-stream, */*"); if (is != null) { // Decode message feedMessage = FeedMessage.PARSER.parseFrom(is); diff --git a/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java index e57c2620e82..06fb118c331 100644 --- a/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java @@ -155,14 +155,10 @@ private TimetableSnapshot getTimetableSnapshot(final boolean force) { /** * Method to apply a trip update list to the most recent version of the timetable snapshot. A * GTFS-RT feed is always applied against a single static feed (indicated by feedId). -<<<<<<< HEAD - * -======= * * However, multi-feed support is not completed and we currently assume there is only one static * feed when matching IDs. * ->>>>>>> 7296be8ffd532a13afb0bec263a9f436ab787022 * @param graph graph to update (needed for adding/changing stop patterns) * @param fullDataset true iff the list with updates represent all updates that are active right * now, i.e. all previous updates should be disregarded @@ -682,7 +678,7 @@ private boolean addTripToGraphAndBuffer(final String feedId, final Graph graph, // TODO: filter/interpolate stop times like in PatternHopFactory? // Create StopPattern - final StopPattern stopPattern = new StopPattern(stopTimes); + final StopPattern stopPattern = new StopPattern(stopTimes, graph.deduplicator); // Get cached trip pattern or create one if it doesn't exist yet final TripPattern pattern = tripPatternCache.getOrCreateTripPattern(stopPattern, trip.getRoute(), graph); diff --git a/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java b/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java index f79dc2467ec..181e50a538e 100644 --- a/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; import org.geotools.data.FeatureSource; import org.geotools.data.Query; import org.geotools.data.wfs.WFSDataStore; diff --git a/src/main/java/org/opentripplanner/updater/traffic/OpenTrafficUpdater.java b/src/main/java/org/opentripplanner/updater/traffic/OpenTrafficUpdater.java deleted file mode 100644 index 6ce34a28bc6..00000000000 --- a/src/main/java/org/opentripplanner/updater/traffic/OpenTrafficUpdater.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.opentripplanner.updater.traffic; - -import com.beust.jcommander.internal.Maps; -import io.opentraffic.engine.data.pbf.ExchangeFormat; -import com.fasterxml.jackson.databind.JsonNode; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.traffic.Segment; -import org.opentripplanner.traffic.SegmentSpeedSample; -import org.opentripplanner.traffic.StreetSpeedSnapshot; -import org.opentripplanner.traffic.StreetSpeedSnapshotSource; -import org.opentripplanner.updater.GraphUpdaterManager; -import org.opentripplanner.updater.PollingGraphUpdater; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.Map; - -/** - * Update the graph with traffic data from OpenTraffic. - */ -public class OpenTrafficUpdater extends PollingGraphUpdater { - private static final Logger LOG = LoggerFactory.getLogger(OpenTrafficUpdater.class); - - private Graph graph; - private GraphUpdaterManager graphUpdaterManager; - - /** the tile directory to search through */ - private File tileDirectory; - - private boolean hasAlreadyRun = false; - - @Override - protected void runPolling() throws Exception { - LOG.info("Loading speed data"); - - // Build a speed index now while we're running in our own thread. We'll swap it out - // at the appropriate time with a GraphWriterRunnable, but no need to synchronize yet. - Map speedIndex = Maps.newHashMap(); - - // search through the tile directory - for (File z : tileDirectory.listFiles()) { - for (File x : z.listFiles()) { - for (File y : x.listFiles()) { - if (!y.getName().endsWith(".traffic.pbf")) { - LOG.warn("Skipping non-traffic file {} in tile directory", y); - continue; - } - - // Deserialize it - InputStream in = new BufferedInputStream(new FileInputStream(y)); - ExchangeFormat.BaselineTile tile = ExchangeFormat.BaselineTile.parseFrom(in); - in.close(); - - // TODO: handle metadata - - for (int i = 0; i < tile.getSegmentsCount(); i++) { - ExchangeFormat.BaselineStats stats = tile.getSegments(i); - SegmentSpeedSample sample; - try { - sample = new SegmentSpeedSample(stats); - } catch (IllegalArgumentException e) { - continue; - } - Segment segment = new Segment(stats.getSegment()); - speedIndex.put(segment, sample); - } - } - } - } - - LOG.info("Indexed {} speed samples", speedIndex.size()); - - graphUpdaterManager.execute(graph -> { - graph.streetSpeedSource.setSnapshot(new StreetSpeedSnapshot(speedIndex)); - }); - } - - @Override - protected void configurePolling(Graph graph, JsonNode config) throws Exception { - this.graph = graph; - tileDirectory = new File(config.get("tileDirectory").asText()); - } - - @Override - public void setGraphUpdaterManager(GraphUpdaterManager updaterManager) { - updaterManager.addUpdater(this); - this.graphUpdaterManager = updaterManager; - } - - @Override - public void setup(Graph graph) { - graph.streetSpeedSource = new StreetSpeedSnapshotSource(); - } - - @Override - public void teardown() { - graphUpdaterManager.execute(graph -> { - graph.streetSpeedSource = null; - }); - } -} diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/GenericGbfsService.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/GenericGbfsService.java index 03a331f80cd..e79fe93b5f7 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/GenericGbfsService.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/GenericGbfsService.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Sets; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.GeometryFactory; import org.opentripplanner.analyst.UnsupportedGeometryException; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.vehicle_rental.VehicleRentalRegion; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java index caac0901626..37e402ec0df 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java @@ -1,13 +1,13 @@ package org.opentripplanner.updater.vehicle_rental; import com.fasterxml.jackson.databind.JsonNode; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.prep.PreparedGeometry; -import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; import org.opentripplanner.graph_builder.linking.StreetSplitter; import org.opentripplanner.routing.edgetype.RentAVehicleOffEdge; import org.opentripplanner.routing.edgetype.RentAVehicleOnEdge; diff --git a/src/main/java/org/opentripplanner/util/GeoJsonUtils.java b/src/main/java/org/opentripplanner/util/GeoJsonUtils.java index 09c575a2213..d59064d42a3 100644 --- a/src/main/java/org/opentripplanner/util/GeoJsonUtils.java +++ b/src/main/java/org/opentripplanner/util/GeoJsonUtils.java @@ -2,12 +2,15 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryCollection; -import com.vividsolutions.jts.geom.GeometryFactory; import org.geojson.Feature; import org.geojson.FeatureCollection; import org.geojson.GeoJsonObject; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.geom.TopologyException; +import org.locationtech.jts.precision.GeometryPrecisionReducer; import org.opentripplanner.analyst.UnsupportedGeometryException; import org.opentripplanner.common.geometry.GeometryUtils; @@ -16,6 +19,13 @@ import java.util.List; public class GeoJsonUtils { + // Create a precision reducer that can be used to reduce geometry precision in case JTS doesn't feel like dealing + // with large amounts of coordinate precision. This will round the geometry precision to a maximum of 7 significant + // digits. + private static GeometryPrecisionReducer precisionReducer = new GeometryPrecisionReducer( + new PrecisionModel(1_000_000) + ); + public static Geometry parsePolygonOrMultiPolygonFromJsonNode( JsonNode jsonNode ) throws UnsupportedGeometryException, IOException { @@ -46,7 +56,17 @@ public static Geometry parsePolygonOrMultiPolygonFromJsonNode( GeometryFactory geometryFactory = new GeometryFactory(); GeometryCollection geometryCollection = (GeometryCollection) geometryFactory.buildGeometry(geometries); - return geometryCollection.union(); + try { + return geometryCollection.union(); + } catch (TopologyException topologyException) { + // Sometimes JTS can fail with valid geometries. Retry with reduced precision and a 0 buffer addition if + // a TopologyException occurs. + // See https://github.com/locationtech/jts/issues/120 + // See https://github.com/locationtech/jts/issues/511 + geometryCollection = (GeometryCollection) precisionReducer.reduce(geometryCollection); + GeometryCollection bufferedGeometryCollection = (GeometryCollection) geometryCollection.buffer(0); + return bufferedGeometryCollection.union(); + } } } } diff --git a/src/main/java/org/opentripplanner/util/InstanceCountingClassResolver.java b/src/main/java/org/opentripplanner/util/InstanceCountingClassResolver.java new file mode 100644 index 00000000000..efd27e32e61 --- /dev/null +++ b/src/main/java/org/opentripplanner/util/InstanceCountingClassResolver.java @@ -0,0 +1,53 @@ +package org.opentripplanner.util; + +import com.esotericsoftware.kryo.Registration; +import com.esotericsoftware.kryo.util.DefaultClassResolver; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; + +/** + * This class is used for configuring and optimizing Kryo configuration. + * Kryo lets you register specific serializers for specific concrete classes, but also has quite a few built-in default + * serializers for individual classes or trees of classes under some interface (e.g. Collection). + * Writing and registering a custom serializer or choosing between a third party class's built-in Externalizable method + * and such a custom serializer, can have significant effects on the speed of serialization and size of the resulting + * serialized data. In order to apply the 80/20 rule it's helpful to have a list of how often various classes are + * serialized in your application. + * + * This class works by wrapping one method in the Kryo framework that is called for every serialized instance: the + * method that looks up the serializer for a given class. It counts how many times each class is looked up, and can + * print out a summary of those frequencies as well as the serializer that was associated with each class. + * + * Note that if you have already registered serializers that are non-recursive, e.g. they write out all the values + * in an internal int array within an ArrayList implementation, those internal fields will of course not be counted. + * + * We could achieve the same thing by extending the ReferenceResolver in the same way, but here in ClassResolver we have + * easy access to the map of Class-Serializer associations. + * + * To use this you need to specify a custom class resolver when creating Kryo: + * Kryo kryo = new Kryo(new InstanceCountingClassResolver(), new MapReferenceResolver(), new DefaultStreamFactory()); + * kryo.setRegistrationRequired(false); + * [...] + * ((InstanceCountingClassResolver)kryo.getClassResolver()).summarize(); + * + * Created by abyrd on 2018-08-29 + */ +public class InstanceCountingClassResolver extends DefaultClassResolver { + + private TObjectIntMap> instanceCounts = new TObjectIntHashMap<>(); + + public Registration getRegistration(Class type) { + instanceCounts.adjustOrPutValue(type, 1, 1); + return super.getRegistration(type); + } + + public void summarize() { + instanceCounts.forEachEntry((classe, count) -> { + Registration registration = getRegistration(classe); + String serializerName = registration.getSerializer().getClass().getSimpleName(); + System.out.println(count + " " + classe.getSimpleName() + " " + serializerName); + return true; + }); + } + +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/util/PolylineEncoder.java b/src/main/java/org/opentripplanner/util/PolylineEncoder.java index e722f52f698..dc63b8fa26c 100644 --- a/src/main/java/org/opentripplanner/util/PolylineEncoder.java +++ b/src/main/java/org/opentripplanner/util/PolylineEncoder.java @@ -6,10 +6,10 @@ import org.opentripplanner.util.model.EncodedPolylineBean; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.MultiLineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; public class PolylineEncoder { diff --git a/src/main/java/org/opentripplanner/util/TranslatedString.java b/src/main/java/org/opentripplanner/util/TranslatedString.java index b7398fb7a42..b845359febd 100644 --- a/src/main/java/org/opentripplanner/util/TranslatedString.java +++ b/src/main/java/org/opentripplanner/util/TranslatedString.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -54,9 +55,14 @@ public static I18NString getI18NString(Map translations) { else { I18NString ret; // Check if we only have one name, even under multiple languages - if (new HashSet<>(translations.values()).size() < 2) { + long numberOfUniqValues = translations.values().stream().distinct().count(); + if(numberOfUniqValues == 0) { + throw new IllegalArgumentException("Map of languages and translations is empty."); + } + else if(numberOfUniqValues == 1) { ret = new NonLocalizedString(translations.values().iterator().next()); - } else { + } + else { ret = new TranslatedString(translations); } intern.put(translations, ret); diff --git a/src/main/java/org/opentripplanner/util/WorldEnvelope.java b/src/main/java/org/opentripplanner/util/WorldEnvelope.java index 78742fc051a..46d298647ef 100644 --- a/src/main/java/org/opentripplanner/util/WorldEnvelope.java +++ b/src/main/java/org/opentripplanner/util/WorldEnvelope.java @@ -1,7 +1,7 @@ package org.opentripplanner.util; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; import java.io.Serializable; diff --git a/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java b/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java index 4d3e4436b3d..e6a9421cf00 100644 --- a/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java +++ b/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java @@ -1,7 +1,7 @@ package org.opentripplanner.visualizer; import com.google.common.collect.Sets; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import javassist.Modifier; import org.opentripplanner.common.model.GenericLocation; import org.opentripplanner.graph_builder.annotation.GraphBuilderAnnotation; diff --git a/src/main/java/org/opentripplanner/visualizer/ShowGraph.java b/src/main/java/org/opentripplanner/visualizer/ShowGraph.java index 44728618702..6c66dec6b25 100644 --- a/src/main/java/org/opentripplanner/visualizer/ShowGraph.java +++ b/src/main/java/org/opentripplanner/visualizer/ShowGraph.java @@ -34,9 +34,9 @@ import processing.core.PApplet; import processing.core.PFont; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.index.strtree.STRtree; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.strtree.STRtree; /** * Processing applet to show a map of the graph. The user can: - Use mouse wheel to zoom (or right drag, or ctrl-drag) - Left drag to pan around the diff --git a/src/test/java/org/opentripplanner/ConstantsForTests.java b/src/test/java/org/opentripplanner/ConstantsForTests.java index 87a1f4f8af8..56ba9f38e4d 100644 --- a/src/test/java/org/opentripplanner/ConstantsForTests.java +++ b/src/test/java/org/opentripplanner/ConstantsForTests.java @@ -2,13 +2,18 @@ import java.io.File; import java.io.IOException; +import java.net.URLDecoder; import java.util.HashMap; import org.opentripplanner.graph_builder.module.GraphBuilderModuleSummary; import org.opentripplanner.model.calendar.CalendarServiceData; +import org.opentripplanner.graph_builder.module.DirectTransferGenerator; import org.opentripplanner.graph_builder.module.StreetLinkerModule; +import org.opentripplanner.graph_builder.module.osm.DefaultWayPropertySetSource; +import org.opentripplanner.graph_builder.module.osm.OpenStreetMapModule; import org.opentripplanner.gtfs.GtfsContext; import org.opentripplanner.gtfs.GtfsLibrary; +import org.opentripplanner.openstreetmap.impl.AnyFileBasedOpenStreetMapProviderImpl; import org.opentripplanner.routing.edgetype.factory.PatternHopFactory; import org.opentripplanner.routing.edgetype.factory.TransferGraphLinker; import org.opentripplanner.routing.graph.Graph; @@ -27,12 +32,18 @@ public class ConstantsForTests { public static final String FARE_COMPONENT_GTFS = "src/test/resources/farecomponent_gtfs.zip"; + public static final String VERMONT_GTFS = "/vermont/ruralcommunity-flex-vt-us.zip"; + + public static final String VERMONT_OSM = "/vermont/vermont-rct.osm.pbf"; + private static ConstantsForTests instance = null; private Graph portlandGraph = null; private GtfsContext portlandContext = null; + private Graph vermontGraph = null; + private ConstantsForTests() { } @@ -80,7 +91,57 @@ private void setupPortland() { StreetLinkerModule ttsnm = new StreetLinkerModule(); ttsnm.buildGraph(portlandGraph, new GraphBuilderModuleSummary(ttsnm)); } - + + public Graph getVermontGraph() { + if (vermontGraph == null) { + vermontGraph = getGraph(VERMONT_OSM, VERMONT_GTFS); + vermontGraph.setUseFlexService(true); + } + return vermontGraph; + } + + private Graph getGraph(String osmFile, String gtfsFile) { + try { + Graph g = new Graph(); + OpenStreetMapModule loader = new OpenStreetMapModule(); + loader.setDefaultWayPropertySetSource(new DefaultWayPropertySetSource()); + AnyFileBasedOpenStreetMapProviderImpl provider = new AnyFileBasedOpenStreetMapProviderImpl(); + + File file = new File( + URLDecoder.decode(this.getClass().getResource(osmFile).getFile(), + "UTF-8")); + + provider.setPath(file); + loader.setProvider(provider); + + loader.buildGraph(g, new GraphBuilderModuleSummary(loader)); + + GtfsContext ctx = GtfsLibrary.readGtfs(new File( + URLDecoder.decode(this.getClass().getResource(gtfsFile).getFile(), + "UTF-8"))); + PatternHopFactory factory = new PatternHopFactory(ctx); + factory.run(g); + + CalendarServiceData csd = createCalendarServiceData(ctx.getOtpTransitService()); + g.putService(CalendarServiceData.class, csd); + g.updateTransitFeedValidity(csd); + g.hasTransit = true; + + DirectTransferGenerator dtg = new DirectTransferGenerator(2000); + dtg.buildGraph(g, new GraphBuilderModuleSummary(dtg)); + + StreetLinkerModule slm = new StreetLinkerModule(); + slm.buildGraph(g, new GraphBuilderModuleSummary(slm)); + + g.index(true); + + return g; + } catch(Exception ex) { + ex.printStackTrace(); + } + return null; + } + public static Graph buildGraph(String path) { GtfsContext context; try { diff --git a/src/test/java/org/opentripplanner/GtfsTest.java b/src/test/java/org/opentripplanner/GtfsTest.java index f8d6c50cbd8..7311f0a56ad 100644 --- a/src/test/java/org/opentripplanner/GtfsTest.java +++ b/src/test/java/org/opentripplanner/GtfsTest.java @@ -103,8 +103,15 @@ public Leg plan(long dateTime, String fromVertex, String toVertex, String onTrip public Leg[] plan(long dateTime, String fromVertex, String toVertex, String onTripId, boolean wheelchairAccessible, boolean preferLeastTransfers, TraverseMode preferredMode, String excludedRoute, String excludedStop, int legCount) { + return plan(dateTime, fromVertex, toVertex, onTripId, wheelchairAccessible, preferLeastTransfers, + preferredMode, excludedRoute, excludedStop, legCount, null); + } + + public Leg[] plan(long dateTime, String fromVertex, String toVertex, String onTripId, + boolean wheelchairAccessible, boolean preferLeastTransfers, TraverseMode preferredMode, + String excludedRoute, String excludedStop, int legCount, RoutingRequest opt) { final TraverseMode mode = preferredMode != null ? preferredMode : TraverseMode.TRANSIT; - RoutingRequest routingRequest = new RoutingRequest(); + RoutingRequest routingRequest = opt == null ? new RoutingRequest() : opt; routingRequest.setNumItineraries(1); routingRequest.setArriveBy(dateTime < 0); @@ -138,6 +145,8 @@ public Leg[] plan(long dateTime, String fromVertex, String toVertex, String onTr routingRequest.setWalkBoardCost(30); List paths = new GraphPathFinder(router).getPaths(routingRequest); + if (paths.isEmpty()) + return new Leg[] { null }; TripPlan tripPlan = GraphPathToTripPlanConverter.generatePlan(paths, routingRequest); // Stored in instance field for use in individual tests itinerary = tripPlan.itinerary.get(0); diff --git a/src/test/java/org/opentripplanner/analyst/InitialStopsTest.java b/src/test/java/org/opentripplanner/analyst/InitialStopsTest.java index 818cf5794d5..6bd9ee3700c 100644 --- a/src/test/java/org/opentripplanner/analyst/InitialStopsTest.java +++ b/src/test/java/org/opentripplanner/analyst/InitialStopsTest.java @@ -3,7 +3,6 @@ import gnu.trove.iterator.TIntIntIterator; import gnu.trove.map.TIntIntMap; import junit.framework.TestCase; -import junit.framework.TestResult; import org.joda.time.LocalDate; import org.junit.Test; import org.opentripplanner.analyst.cluster.TaskStatistics; @@ -11,9 +10,6 @@ import org.opentripplanner.profile.ProfileRequest; import org.opentripplanner.profile.RaptorWorkerData; import org.opentripplanner.profile.RepeatedRaptorProfileRouter; -import org.opentripplanner.profile.TimeWindow; -import org.opentripplanner.routing.algorithm.AStar; -import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.TraverseModeSet; import org.opentripplanner.routing.graph.Graph; diff --git a/src/test/java/org/opentripplanner/analyst/broker/RedeliveryTest.java b/src/test/java/org/opentripplanner/analyst/broker/RedeliveryTest.java deleted file mode 100644 index e47a4c42c07..00000000000 --- a/src/test/java/org/opentripplanner/analyst/broker/RedeliveryTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.opentripplanner.analyst.broker; - -import org.opentripplanner.analyst.cluster.AnalystWorker; -import org.opentripplanner.analyst.cluster.JobSimulator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * This test is not an automatic unit test. It is an integration test that must be started manually, because it takes - * a long time to run. It will start up a broker and some local workers, then submit a large job to the broker. The - * workers will fail to complete tasks some percentage of the time, but eventually the whole job should be finished - * because the broker will re-send tasks. - */ -public class RedeliveryTest { - - private static final Logger LOG = LoggerFactory.getLogger(RedeliveryTest.class); - static final int N_TASKS = 100; - static final int N_WORKERS = 4; - static final int FAILURE_RATE = 20; // percent - - public static void main(String[] params) { - - // Start a broker in a new thread. - Properties brokerConfig = new Properties(); - brokerConfig.setProperty("graphs-bucket", "FAKE"); - brokerConfig.setProperty("pointsets-bucket", "FAKE"); - brokerConfig.setProperty("work-offline", "true"); - BrokerMain brokerMain = new BrokerMain(brokerConfig); - Thread brokerThread = new Thread(brokerMain); // TODO combine broker and brokermain, set offline mode. - brokerThread.start(); - - // Start some workers. - Properties workerConfig = new Properties(); - workerConfig.setProperty("initial-graph-id", "GRAPH"); - List workerThreads = new ArrayList<>(); - for (int i = 0; i < N_WORKERS; i++) { - AnalystWorker worker = new AnalystWorker(workerConfig); - worker.dryRunFailureRate = FAILURE_RATE; - Thread workerThread = new Thread(worker); - workerThreads.add(workerThread); - workerThread.start(); - } - - // Feed some work to the broker. - JobSimulator jobSimulator = new JobSimulator(); - jobSimulator.nOrigins = N_TASKS; - jobSimulator.graphId = "GRAPH"; - jobSimulator.sendFakeJob(); - - // Wait for all tasks to be marked finished - while (brokerMain.broker.anyJobsActive()) { - try { - LOG.info("Some jobs are still not complete."); - Thread.sleep(2000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - LOG.info("All jobs finished."); - System.exit(0); - } - -} diff --git a/src/test/java/org/opentripplanner/analyst/scenario/AddTripPatternTest.java b/src/test/java/org/opentripplanner/analyst/scenario/AddTripPatternTest.java deleted file mode 100644 index e7d61e16191..00000000000 --- a/src/test/java/org/opentripplanner/analyst/scenario/AddTripPatternTest.java +++ /dev/null @@ -1,346 +0,0 @@ -package org.opentripplanner.analyst.scenario; - -import com.google.common.collect.Lists; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.io.WKTReader; -import gnu.trove.iterator.TObjectIntIterator; -import gnu.trove.map.TIntIntMap; -import junit.framework.TestCase; -import org.joda.time.LocalDate; -import org.junit.Test; -import org.opentripplanner.common.model.GenericLocation; -import org.opentripplanner.profile.*; -import org.opentripplanner.routing.algorithm.AStar; -import org.opentripplanner.routing.core.RoutingRequest; -import org.opentripplanner.routing.core.TraverseMode; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.graph.Vertex; -import org.opentripplanner.routing.spt.ShortestPathTree; - -import java.time.DayOfWeek; -import java.util.Arrays; -import java.util.BitSet; - -import static org.opentripplanner.graph_builder.module.FakeGraph.*; - -/** - * Test adding trip patterns. - */ -public class AddTripPatternTest extends TestCase { - /** Make sure that stops are properly linked into the graph */ - @Test - public void testStopLinking () throws Exception { - AddTripPattern atp = getAddTripPattern(RouteSelector.BROAD_HIGH); - atp.timetables.add(getTimetable(true)); - - // get a graph - Graph g = buildGraphNoTransit(); - indexGraphAndLinkStations(g); - - // materialize the trip pattern - atp.materialize(g); - - // there should be five stops because one point is not a stop - assertEquals(5, atp.temporaryStops.length); - - // they should all be linked into the graph - for (int i = 0; i < atp.temporaryStops.length; i++) { - assertNotNull(atp.temporaryStops[i].sample); - assertNotNull(atp.temporaryStops[i].sample.v0); - assertNotNull(atp.temporaryStops[i].sample.v1); - } - - - // no services running: not needed for trips added on the fly. - TimeWindow window = new TimeWindow(7 * 3600, 9 * 3600, new BitSet(), DayOfWeek.WEDNESDAY); - - Scenario scenario = new Scenario(0); - scenario.modifications = Lists.newArrayList(atp); - ProfileRequest req = new ProfileRequest(); - req.scenario = scenario; - req.boardingAssumption = RaptorWorkerTimetable.BoardingAssumption.WORST_CASE; - - RaptorWorkerData data = new RaptorWorkerData(g, window, req); - assertEquals(5, data.nStops); - - // make sure we can find the stops - AStar aStar = new AStar(); - RoutingRequest rr = new RoutingRequest(TraverseMode.WALK); - rr.from = new GenericLocation(39.963417, -82.980799); - rr.batch = true; - rr.setRoutingContext(g); - rr.batch = true; - - ShortestPathTree spt = aStar.getShortestPathTree(rr); - - TIntIntMap stops = data.findStopsNear(spt, g, false, 1.3f); - - // we should have found stops - assertFalse(stops.isEmpty()); - - // ensure that the times made it into the data - // This assumes worst-case departure, and the first worst departure is 10:30 after the service - // starts running (dwell + headway) - assertEquals(4 * 3600 + 600 + 30, - data.timetablesForPattern.get(0).getFrequencyDeparture(0, 0, 39 * 360, - -1, null)); - } - - /** Test adding trips with a timetable rather than frequencies */ - @Test - public void testTimetableTrips () throws Exception { - AddTripPattern atp = getAddTripPattern(RouteSelector.BROAD_HIGH); - atp.timetables.add(getTimetable(false)); - - // get a graph - Graph g = buildGraphNoTransit(); - indexGraphAndLinkStations(g); - - // materialize the trip pattern - atp.materialize(g); - - // there should be five stops because one point is not a stop - assertEquals(5, atp.temporaryStops.length); - - // they should all be linked into the graph - for (int i = 0; i < atp.temporaryStops.length; i++) { - assertNotNull(atp.temporaryStops[i].sample); - assertNotNull(atp.temporaryStops[i].sample.v0); - assertNotNull(atp.temporaryStops[i].sample.v1); - } - - - // no services running: not needed for trips added on the fly. - TimeWindow window = new TimeWindow(7 * 3600, 9 * 3600, new BitSet(), DayOfWeek.WEDNESDAY); - - Scenario scenario = new Scenario(0); - scenario.modifications = Lists.newArrayList(atp); - ProfileRequest req = new ProfileRequest(); - req.scenario = scenario; - req.boardingAssumption = RaptorWorkerTimetable.BoardingAssumption.WORST_CASE; - - RaptorWorkerData data = new RaptorWorkerData(g, window, req); - - assertEquals(5, data.nStops); - - // make sure we can find the stops - AStar aStar = new AStar(); - RoutingRequest rr = new RoutingRequest(TraverseMode.WALK); - rr.from = new GenericLocation(39.963417, -82.980799); - rr.batch = true; - rr.setRoutingContext(g); - rr.batch = true; - - ShortestPathTree spt = aStar.getShortestPathTree(rr); - - TIntIntMap stops = data.findStopsNear(spt, g, false, 1.3f); - - // we should have found stops - assertFalse(stops.isEmpty()); - - // ensure that the times made it into the data - // This is after the first dwell time has been applied - assertEquals(7 * 3600 + 30, data.timetablesForPattern.get(0).getDeparture(0, 0)); - } - - /** Make sure that transfers work */ - @Test - public void testTransfers () throws Exception { - AddTripPattern atp = getAddTripPattern(RouteSelector.BROAD_HIGH); - atp.timetables.add(getTimetable(false)); - - AddTripPattern atp2 = getAddTripPattern(RouteSelector.BEXLEY_CMH); - atp2.timetables.add(getTimetable(true)); - - // get a graph - Graph g = buildGraphNoTransit(); - addTransit(g); - indexGraphAndLinkStations(g); - - // materialize the trip pattern - atp.materialize(g); - atp2.materialize(g); - - TimeWindow window = new TimeWindow(7 * 3600, 9 * 3600, g.index.servicesRunning(new LocalDate(2015, 6, 10)), DayOfWeek.WEDNESDAY); - - Scenario scenario = new Scenario(0); - scenario.modifications = Lists.newArrayList(atp, atp2); - ProfileRequest req = new ProfileRequest(); - req.scenario = scenario; - req.boardingAssumption = RaptorWorkerTimetable.BoardingAssumption.WORST_CASE; - - RaptorWorkerData data = new RaptorWorkerData(g, window, req); - - // make sure that we have transfers a) between the new lines b) from the new lines - // to the existing lines c) from the existing lines to the new lines - // stop IDs in the data will be 0 and 1 for existing stops, 2 - 6 for Broad/High and 7 - 11 for Bexley/CMH - int[] txFromExisting = data.transfersForStop.get(0); - if (txFromExisting.length == 0) - txFromExisting = data.transfersForStop.get(1); - - // make sure there's a transfer to stop 4 (Broad/High) - // the AddTripPattern instructions are processed in order - // also recall that each transfer has two ints in the array as it's a jagged array of - // dest_pattern, distance - assertTrue(txFromExisting.length > 0); - - boolean foundTx = false; - - for (int i = 0; i < txFromExisting.length; i += 2) { - if (txFromExisting[i] == 4) { - foundTx = true; - break; - } - } - - assertTrue("transfer from existing to new", foundTx); - - // Check that there are transfers from the new route to the existing route - // This is the stop at Broad and High - int[] txToExisting = data.transfersForStop.get(4); - assertTrue(txToExisting.length > 0); - foundTx = false; - - for (int i = 0; i < txToExisting.length; i += 2) { - if (txToExisting[i] == 0 || txToExisting[i] == 1) { - // stop from existing route - foundTx = true; - break; - } - } - - assertTrue("transfer from new to existing", foundTx); - - // Check that there are transfers between the new routes - int[] txBetweenNew = data.transfersForStop.get(7); - assertTrue(txBetweenNew.length > 0); - foundTx = false; - - for (int i = 0; i < txBetweenNew.length; i += 2) { - if (txBetweenNew[i] == 2) { - foundTx = true; - break; - } - } - - assertTrue(foundTx); - } - - /** Test the full routing */ - @Test - public void integrationTest () throws Exception { - Graph g = buildGraphNoTransit(); - addTransit(g); - indexGraphAndLinkStations(g); - - ProfileRequest pr1 = new ProfileRequest(); - pr1.date = new LocalDate(2015, 6, 10); - pr1.fromTime = 7 * 3600; - pr1.toTime = 9 * 3600; - pr1.fromLat = pr1.toLat = 39.9621; - pr1.fromLon = pr1.toLon = -83.0007; - - RepeatedRaptorProfileRouter rrpr1 = new RepeatedRaptorProfileRouter(g, pr1); - rrpr1.route(); - - ProfileRequest pr2 = new ProfileRequest(); - pr2.date = new LocalDate(2015, 6, 10); - pr2.fromTime = 7 * 3600; - pr2.toTime = 9 * 3600; - pr2.fromLat = pr2.toLat = 39.9621; - pr2.fromLon = pr2.toLon = -83.0007; - - AddTripPattern atp = getAddTripPattern(RouteSelector.BROAD_HIGH); - atp.timetables.add(getTimetable(true)); - - pr2.scenario = new Scenario(0); - pr2.scenario.modifications = Arrays.asList(atp); - - RepeatedRaptorProfileRouter rrpr2 = new RepeatedRaptorProfileRouter(g, pr2); - rrpr2.route(); - - boolean foundDecrease = false; - - // make sure that travel time did not increase - for (TObjectIntIterator vit = rrpr1.timeSurfaceRangeSet.min.times.iterator(); vit.hasNext();) { - vit.advance(); - - int time2 = rrpr2.timeSurfaceRangeSet.min.getTime(vit.key()); - - assertTrue(time2 <= vit.value()); - - if (time2 < vit.value()) foundDecrease = true; - } - - assertTrue("found decreases in travel time due to adding route", foundDecrease); - } - - private AddTripPattern getAddTripPattern (RouteSelector sel) throws Exception { - AddTripPattern atp = new AddTripPattern(); - WKTReader wr = new WKTReader(); - atp.geometry = sel.getGeometry(); - - atp.name = "Broad High Express"; - atp.stops = new BitSet(); - - // everything is a stop except for (0-based) point 3, on High one block north of Broad - // or on East Broad, depending on geometry chosen - for (int i = 0; i < 6; i++) { - if (i == 3) - atp.stops.clear(i); - else - atp.stops.set(i); - } - - atp.timetables = Lists.newArrayList(); - - return atp; - } - - /** - * Get a timetable. If frequency = true, run every 10 minutes from 4AM to 10PM local. - * If frequency = false, one run at 7 AM. - */ - private AddTripPattern.PatternTimetable getTimetable (boolean frequency) { - AddTripPattern.PatternTimetable tt = new AddTripPattern.PatternTimetable(); - tt.days = new BitSet(); - // wednesday service only - tt.days.set(2); - - tt.dwellTimes = new int[] { 30, 30, 30, 30, 30 }; - tt.hopTimes = new int[] { 90, 90, 90, 90 }; - - tt.frequency = frequency; - - if (frequency) { - tt.startTime = 4 * 3600; - tt.endTime = 22 * 3600; - tt.headwaySecs = 600; - } - else - tt.startTime = 7 * 3600; - - return tt; - } - - /** Just a switch between two different route geometries */ - private static enum RouteSelector { - BROAD_HIGH, BEXLEY_CMH; - - public LineString getGeometry () throws Exception { - WKTReader wr = new WKTReader(); - switch (this) { - // Running west on Broad Street from Bexley to High, and then north to the Short North, - // in Columbus, OH - case BROAD_HIGH: - return (LineString) wr.read("LINESTRING(-82.93727602046642744 39.96934234865877045, -82.98058356730228979 39.96435660398918088, -82.99808255349556418 39.96259692939991481, -83.00091758477827852 39.96357452639394836, -83.00492573245382744 39.98283318717648882, -83.00639212794487776 39.99065396312879273)"); - // Running east on Broad Street and then up Hamilton to Sawyer and the CMH airport - case BEXLEY_CMH: - return (LineString) wr.read("LINESTRING(-82.93296696 39.96964327, -82.91302398 39.97218502, -82.88408711 39.97492229, -82.8739201 39.97570437, -82.86864107 39.99603838, -82.89444963 39.99897118)"); - default: - // can't happen - return null; - } - } - } -} diff --git a/src/test/java/org/opentripplanner/analyst/scenario/TimetableFilterTest.java b/src/test/java/org/opentripplanner/analyst/scenario/TimetableFilterTest.java deleted file mode 100644 index 43db0ff3f26..00000000000 --- a/src/test/java/org/opentripplanner/analyst/scenario/TimetableFilterTest.java +++ /dev/null @@ -1,432 +0,0 @@ -package org.opentripplanner.analyst.scenario; - -import junit.framework.TestCase; -import org.junit.Test; -import org.opentripplanner.model.Agency; -import org.opentripplanner.model.FeedScopedId; -import org.opentripplanner.model.Route; -import org.opentripplanner.model.Stop; -import org.opentripplanner.model.StopTime; -import org.opentripplanner.model.StopPattern; -import org.opentripplanner.model.Trip; -import org.opentripplanner.routing.edgetype.TripPattern; -import org.opentripplanner.routing.trippattern.Deduplicator; -import org.opentripplanner.routing.trippattern.FrequencyEntry; -import org.opentripplanner.routing.trippattern.TripTimes; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -/** - * Tests for various timetable filters - */ -public class TimetableFilterTest extends TestCase { - private TripPattern pattern, metroPattern; - private Trip trip, trip2, metroTrip; - private TripTimes times, metroTimes; - private FrequencyEntry frequencyEntry; - private Stop[] stops; - private Agency agency; - private Route route, metro; - - /* ROUTE REMOVAL (also matching) */ - - /** test that routes that should be removed are */ - @Test - public void testRouteRemoval () { - RemoveTrip rt = new RemoveTrip(); - rt.agencyId = agency.getId(); - rt.routeId = Arrays.asList(route.getId().getId()); - - assertNull(rt.apply(trip, pattern, times)); - assertNull(rt.apply(trip, pattern, frequencyEntry)); - } - - /** test that routes removed by route type work */ - @Test - public void testTripRemovalByRouteType () { - RemoveTrip rt = new RemoveTrip(); - rt.agencyId = agency.getId(); - rt.routeType = new int[] { com.conveyal.gtfs.model.Route.SUBWAY }; - - assertNull(rt.apply(metroTrip, metroPattern, metroTimes)); - assertNotNull(rt.apply(trip, pattern, times)); - } - - /** test that routes that should not be removed are not */ - @Test - public void testRoutePreservation () { - RemoveTrip rt = new RemoveTrip(); - rt.agencyId = agency.getId(); - rt.routeId = Arrays.asList("SOMETHING ELSE"); - - assertEquals(times, rt.apply(trip, pattern, times)); - assertEquals(frequencyEntry, rt.apply(trip, pattern, frequencyEntry)); - } - - /** Test that trips that should be removed are */ - @Test - public void testTripRemoval () { - RemoveTrip rt = new RemoveTrip(); - rt.agencyId = agency.getId(); - rt.tripId = Arrays.asList(trip.getId().getId()); - - assertNull(rt.apply(trip, pattern, times)); - assertNull(rt.apply(trip, pattern, frequencyEntry)); - } - - /** test that trips that should not be removed are not */ - @Test - public void testTripPreservation () { - RemoveTrip rt = new RemoveTrip(); - rt.agencyId = agency.getId(); - rt.tripId = Arrays.asList("SOMETHING ELSE"); - - assertEquals(times, rt.apply(trip, pattern, times)); - assertEquals(frequencyEntry, rt.apply(trip, pattern, frequencyEntry)); - - rt.agencyId = "NOT THE AGENCY ID"; - rt.tripId = Arrays.asList(trip.getId().getId()); - - assertEquals(times, rt.apply(trip, pattern, times)); - assertEquals(frequencyEntry, rt.apply(trip, pattern, frequencyEntry)); - } - - /** test a highly specific match */ - @Test - public void testSpecificRemoval () { - // route ID and trip ID are supposed to combined with a logical and, ensure that they are. - RemoveTrip rt = new RemoveTrip(); - rt.agencyId = agency.getId(); - rt.routeId = Arrays.asList(route.getId().getId()); - rt.tripId = Arrays.asList(trip.getId().getId()); - - assertNull(rt.apply(trip, pattern, times)); - assertNull(rt.apply(trip, pattern, frequencyEntry)); - - rt.routeId = Arrays.asList("SOMETHING ELSE"); - - assertEquals(times, rt.apply(trip, pattern, times)); - assertEquals(frequencyEntry, rt.apply(trip, pattern, frequencyEntry)); - - rt.tripId = Arrays.asList("SOMETHING ELSE"); - - assertEquals(times, rt.apply(trip, pattern, times)); - assertEquals(frequencyEntry, rt.apply(trip, pattern, frequencyEntry)); - - rt.routeId = Arrays.asList(route.getId().getId()); - - assertEquals(times, rt.apply(trip, pattern, times)); - assertEquals(frequencyEntry, rt.apply(trip, pattern, frequencyEntry)); - } - - /** Test modification of dwell times */ - @Test - public void testDwellTimes () { - AdjustDwellTime adt = new AdjustDwellTime(); - adt.routeId = Arrays.asList(route.getId().getId()); - adt.agencyId = agency.getId(); - adt.stopId = Arrays.asList(stops[0].getId().getId(), stops[2].getId().getId()); - adt.dwellTime = 60; - - TripTimes tt2 = adt.apply(trip, pattern, times); - assertNotNull(tt2); - assertEquals(times.getArrivalTime(0), tt2.getArrivalTime(0)); - assertEquals(60, tt2.getDepartureTime(0) - tt2.getArrivalTime(0)); - assertEquals(30, tt2.getDepartureTime(1) - tt2.getArrivalTime(1)); - assertEquals(60, tt2.getDepartureTime(2) - tt2.getArrivalTime(2)); - assertEquals(30, tt2.getDepartureTime(3) - tt2.getArrivalTime(3)); - - // make sure we didn't accidentally modify the orignal times - assertEquals(30, times.getDepartureTime(2) - times.getArrivalTime(2)); - - FrequencyEntry fe2 = adt.apply(trip, pattern, frequencyEntry); - assertNotNull(fe2); - - tt2 = fe2.tripTimes; - - assertEquals(60, tt2.getDepartureTime(0) - tt2.getArrivalTime(0)); - assertEquals(30, tt2.getDepartureTime(1) - tt2.getArrivalTime(1)); - assertEquals(60, tt2.getDepartureTime(2) - tt2.getArrivalTime(2)); - assertEquals(30, tt2.getDepartureTime(3) - tt2.getArrivalTime(3)); - - // make sure we didn't accidentally modify the original times - assertEquals(30, frequencyEntry.tripTimes.getDepartureTime(2) - frequencyEntry.tripTimes.getArrivalTime(2)); - - // wildcard - adt.stopId = null; - - tt2 = adt.apply(trip, pattern, times); - assertNotNull(tt2); - assertEquals(times.getArrivalTime(0), tt2.getArrivalTime(0)); - assertEquals(60, tt2.getDepartureTime(0) - tt2.getArrivalTime(0)); - assertEquals(60, tt2.getDepartureTime(1) - tt2.getArrivalTime(1)); - assertEquals(60, tt2.getDepartureTime(2) - tt2.getArrivalTime(2)); - assertEquals(60, tt2.getDepartureTime(3) - tt2.getArrivalTime(3)); - - // test repeated application - adt.stopId = Arrays.asList(stops[2].getId().getId()); - adt.dwellTime = 17; - - tt2 = adt.apply(trip, pattern, tt2); - assertNotNull(tt2); - assertEquals(times.getArrivalTime(0), tt2.getArrivalTime(0)); - assertEquals(60, tt2.getDepartureTime(0) - tt2.getArrivalTime(0)); - assertEquals(60, tt2.getDepartureTime(1) - tt2.getArrivalTime(1)); - assertEquals(17, tt2.getDepartureTime(2) - tt2.getArrivalTime(2)); - assertEquals(60, tt2.getDepartureTime(3) - tt2.getArrivalTime(3)); - } - - /** test modifying frequencies */ - @Test - public void testAdjustHeadway () { - AdjustHeadway ah = new AdjustHeadway(); - ah.agencyId = agency.getId(); - ah.routeId = Arrays.asList(route.getId().getId()); - ah.headway = 120; - - // should have no effect on scheduled trips - assertEquals(times, ah.apply(trip, pattern, times)); - - FrequencyEntry fe2 = ah.apply(trip, pattern, frequencyEntry); - assertNotNull(fe2); - assertEquals(120, fe2.headway); - // make sure we didn't accidentally modify the entry in the graph - assertEquals(600, frequencyEntry.headway); - } - - /** test modifying trip patterns */ - @Test - public void testSkipStopInMiddle () { - SkipStop ss = new SkipStop(); - ss.routeId = Arrays.asList(route.getId().getId()); - ss.agencyId = agency.getId(); - ss.stopId = Arrays.asList(stops[2].getId().getId()); - - Collection result = ss.apply(pattern); - - assertEquals(1, result.size()); - - TripPattern newtp = result.iterator().next(); - - assertNotSame(pattern, newtp); - - // TODO getNumScheduledTrips is zero - why? - assertEquals(2, newtp.scheduledTimetable.tripTimes.size()); - assertEquals(2, newtp.scheduledTimetable.frequencyEntries.size()); - - assertEquals(3, newtp.stopPattern.size); - - // make sure the times are correct - assertEquals(pattern.scheduledTimetable.tripTimes.get(0).getDepartureTime(0), - newtp.scheduledTimetable.tripTimes.get(0).getDepartureTime(0)); - // after the skipped stop: dwell times should be removed - assertEquals(pattern.scheduledTimetable.tripTimes.get(0).getDepartureTime(3) - 30, - newtp.scheduledTimetable.tripTimes.get(0).getDepartureTime(2)); - - assertEquals(pattern.stopPattern.stops[3], newtp.stopPattern.stops[2]); - - // and for the frequency entry - // make sure the times are correct - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(0), - newtp.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(0)); - // after the skipped stop: dwell times should be removed - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(3) - 30, - newtp.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(2)); - } - - /** test modifying one trip on a trip pattern */ - @Test - public void testModifySingleTrip () { - SkipStop ss = new SkipStop(); - ss.routeId = Arrays.asList(route.getId().getId()); - ss.agencyId = agency.getId(); - ss.tripId = Arrays.asList(trip.getId().getId()); - ss.stopId = Arrays.asList(stops[2].getId().getId()); - - Collection result = ss.apply(pattern); - - assertEquals(2, result.size()); - - Iterator tpit = result.iterator(); - // non-ideal: assuming defined order. - TripPattern newtp = tpit.next(); - TripPattern clone = tpit.next(); - - assertNotSame(pattern, newtp); - assertNotSame(pattern, clone); - - assertEquals(1, newtp.scheduledTimetable.tripTimes.size()); - assertEquals(1, newtp.scheduledTimetable.frequencyEntries.size()); - - assertEquals(3, newtp.stopPattern.size); - - // make sure the times are correct - assertEquals(pattern.scheduledTimetable.tripTimes.get(0).getDepartureTime(0), - newtp.scheduledTimetable.tripTimes.get(0).getDepartureTime(0)); - // after the skipped stop: dwell times should be removed - assertEquals(pattern.scheduledTimetable.tripTimes.get(0).getDepartureTime(3) - 30, - newtp.scheduledTimetable.tripTimes.get(0).getDepartureTime(2)); - - assertEquals(pattern.stopPattern.stops[3], newtp.stopPattern.stops[2]); - - // and for the frequency entry - // make sure the times are correct - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(0), - newtp.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(0)); - // after the skipped stop: dwell times should be removed - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(3) - 30, - newtp.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(2)); - - // make sure the times are correct on the trips that were not modified - assertEquals(pattern.scheduledTimetable.tripTimes.get(0).getDepartureTime(0), - clone.scheduledTimetable.tripTimes.get(0).getDepartureTime(0)); - // after the skipped stop: dwell times should be removed - assertEquals(pattern.scheduledTimetable.tripTimes.get(0).getDepartureTime(3), - clone.scheduledTimetable.tripTimes.get(0).getDepartureTime(3)); - - assertEquals(pattern.stopPattern.stops[3], clone.stopPattern.stops[3]); - - // and for the frequency entry - // make sure the times are correct - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(0), - clone.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(0)); - // after the skipped stop: dwell times should be removed - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(3), - clone.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(3)); - } - @Test - public void testSkipStopsAtStart () { - SkipStop ss = new SkipStop(); - ss.routeId = Arrays.asList(route.getId().getId()); - ss.agencyId = agency.getId(); - ss.stopId = Arrays.asList(stops[0].getId().getId(), stops[1].getId().getId()); - - Collection result = ss.apply(pattern); - - assertEquals(1, result.size()); - - TripPattern newtp = result.iterator().next(); - - assertNotSame(pattern, newtp); - - assertEquals(2, newtp.scheduledTimetable.tripTimes.size()); - assertEquals(2, newtp.scheduledTimetable.frequencyEntries.size()); - - assertEquals(2, newtp.stopPattern.size); - - // make sure the times are correct - // Note that there should be no dwell compression; the start of the trip should simply be chopped off. - assertEquals(pattern.scheduledTimetable.getTripTimes(0).getDepartureTime(2), - newtp.scheduledTimetable.getTripTimes(0).getDepartureTime(0)); - assertEquals(pattern.scheduledTimetable.getTripTimes(0).getDepartureTime(3), - newtp.scheduledTimetable.getTripTimes(0).getDepartureTime(1)); - - assertEquals(pattern.stopPattern.stops[3], newtp.stopPattern.stops[1]); - - // and for the frequency entry - // make sure the times are correct - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(2), - newtp.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(0)); - // after the skipped stop: dwell times should be removed - assertEquals(pattern.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(3), - newtp.scheduledTimetable.frequencyEntries.get(0).tripTimes.getDepartureTime(1)); - } - - @Override - protected void setUp () { - agency = new Agency(); - agency.setId("AGENCY"); - route = new Route(); - route.setType(com.conveyal.gtfs.model.Route.BUS); - route.setShortName("T"); - route.setLongName("TEST"); - route.setAgency(agency); - route.setId(new FeedScopedId(agency.getId(), "TEST")); - - metro = new Route(); - metro.setType(com.conveyal.gtfs.model.Route.SUBWAY); - metro.setShortName("M"); - metro.setLongName("METRO"); - metro.setAgency(agency); - metro.setId(new FeedScopedId(agency.getId(), "METRO")); - - trip = new Trip(); - trip.setRoute(route); - trip.setId(new FeedScopedId(agency.getId(), "TRIP")); - - trip2 = new Trip(); - trip2.setRoute(route); - trip2.setId(new FeedScopedId(agency.getId(), "TRIP2")); - - - stops = new Stop[4]; - - for (int i = 0; i < stops.length; i++) { - Stop s = new Stop(); - s.setLat(-122.123); - s.setLon(37.363 + i * 0.001); - s.setId(new FeedScopedId(agency.getId(), "" + i)); - stops[i] = s; - } - - List stopTimes = makeStopTimes(trip); - StopPattern sp = new StopPattern(stopTimes); - - pattern = new TripPattern(route, sp); - - // make a triptimes - times = makeTripTimes(trip, stopTimes); - pattern.scheduledTimetable.addTripTimes(times); - pattern.scheduledTimetable.addTripTimes(makeTripTimes(trip2, makeStopTimes(trip2))); - - // ten-minute frequency - frequencyEntry = new FrequencyEntry(7 * 3600, 12 * 3600, 600, false, makeTripTimes(trip, makeStopTimes(trip))); - pattern.scheduledTimetable.addFrequencyEntry(frequencyEntry); - pattern.scheduledTimetable.addFrequencyEntry(new FrequencyEntry(7 * 3600, 12 * 3600, 600, false, makeTripTimes(trip2, makeStopTimes(trip2)))); - - metroTrip = new Trip(); - metroTrip.setRoute(metro); - metroTrip.setId(new FeedScopedId(agency.getId(), "TRIP")); - - stopTimes = makeStopTimes(metroTrip); - sp = new StopPattern(stopTimes); - - metroPattern = new TripPattern(metro, sp); - metroTimes = makeTripTimes(metroTrip, stopTimes); - metroPattern.scheduledTimetable.addTripTimes(metroTimes); - } - - /** Make up some trip times. Dwell is 30s, hop is 120s */ - private TripTimes makeTripTimes (Trip trip, List stopTimes) { - return new TripTimes(trip, makeStopTimes(trip), new Deduplicator()); - } - - private List makeStopTimes (Trip trip) { - StopTime[] stopTimes = new StopTime[stops.length]; - int cumulativeTime = 7 * 3600; - - for (int i = 0; i < stops.length; i++) { - Stop stop = stops[i]; - StopTime st = new StopTime(); - st.setStop(stop); - st.setArrivalTime(cumulativeTime); - // dwell time is 30 secs - cumulativeTime += 30; - st.setDepartureTime(cumulativeTime); - // hop time is 2 minutes - cumulativeTime += 120; - - st.setPickupType(StopPattern.PICKDROP_SCHEDULED); - st.setDropOffType(StopPattern.PICKDROP_SCHEDULED); - - st.setTrip(trip); - - stopTimes[i] = st; - } - - return Arrays.asList(stopTimes); - } -} diff --git a/src/test/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverterTest.java b/src/test/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverterTest.java index 1c06f23064e..54a9797a688 100644 --- a/src/test/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverterTest.java +++ b/src/test/java/org/opentripplanner/api/resource/GraphPathToTripPlanConverterTest.java @@ -5,9 +5,9 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.junit.Test; import org.opentripplanner.calendar.impl.CalendarServiceImpl; import org.opentripplanner.model.Agency; @@ -343,13 +343,13 @@ private GraphPath[] buildPaths() { trainStopDepartTime.setTrip(firstTrip); trainStopDepartTime.setStop(trainStopDepart); trainStopDepartTime.setStopSequence(Integer.MIN_VALUE); - trainStopDepartTime.setDepartureTime(4); + trainStopDepartTime.setDepartureTime(10); trainStopDepartTime.setPickupType(3); trainStopDwellTime.setTrip(firstTrip); trainStopDwellTime.setStop(trainStopDwell); trainStopDwellTime.setStopSequence(0); - trainStopDwellTime.setArrivalTime(8); - trainStopDwellTime.setDepartureTime(12); + trainStopDwellTime.setArrivalTime(12); + trainStopDwellTime.setDepartureTime(13); trainStopInterlineFirstTime.setTrip(firstTrip); trainStopInterlineFirstTime.setStop(trainStopInterline); trainStopInterlineFirstTime.setStopSequence(Integer.MAX_VALUE); @@ -366,12 +366,12 @@ private GraphPath[] buildPaths() { ferryStopDepartTime.setTrip(thirdTrip); ferryStopDepartTime.setStop(ferryStopDepart); ferryStopDepartTime.setStopSequence(-1); - ferryStopDepartTime.setDepartureTime(32); + ferryStopDepartTime.setDepartureTime(36); ferryStopDepartTime.setPickupType(2); ferryStopArriveTime.setTrip(thirdTrip); ferryStopArriveTime.setStop(ferryStopArrive); ferryStopArriveTime.setStopSequence(0); - ferryStopArriveTime.setArrivalTime(36); + ferryStopArriveTime.setArrivalTime(38); ferryStopArriveTime.setDropOffType(3); ArrayList firstStopTimes = new ArrayList(); @@ -750,7 +750,7 @@ private GraphPath[] buildPaths() { backwardContext.serviceDays.add(serviceDay); backwardOptions.rctx = backwardContext; - backwardOptions.dateTime = 60L; + backwardOptions.dateTime = 70L; backwardOptions.bikeRentalPickupTime = 4; backwardOptions.bikeRentalDropoffTime = 2; backwardOptions.setArriveBy(true); @@ -973,23 +973,32 @@ private void compare(Itinerary itinerary, Type type) { /** Compare all simple itinerary fields to their expected values. */ private void compareItinerary(Itinerary itinerary, Type type) { - if (type == Type.FORWARD || type == Type.BACKWARD) { - assertEquals(60.0, itinerary.duration.doubleValue(), 0.0); + if (type == Type.FORWARD) { + assertEquals(66.0, itinerary.duration.doubleValue(), 0.0); assertEquals(0L, itinerary.startTime.getTimeInMillis()); + assertEquals(66000L, itinerary.endTime.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(68.0, itinerary.duration.doubleValue(), 0.0); + assertEquals(2000L, itinerary.startTime.getTimeInMillis()); + assertEquals(70000L, itinerary.endTime.getTimeInMillis()); } else if (type == Type.ONBOARD) { - assertEquals(54.0, itinerary.duration.doubleValue(), 0.0); + assertEquals(60.0, itinerary.duration.doubleValue(), 0.0); assertEquals(6000L, itinerary.startTime.getTimeInMillis()); + assertEquals(66000L, itinerary.endTime.getTimeInMillis()); } - assertEquals(60000L, itinerary.endTime.getTimeInMillis()); - if (type == Type.FORWARD || type == Type.BACKWARD) { - assertEquals(27L, itinerary.walkTime); - assertEquals(23L, itinerary.transitTime); - assertEquals(10L, itinerary.waitingTime); + if (type == Type.FORWARD) { + assertEquals(37L, itinerary.walkTime); + assertEquals(17L, itinerary.transitTime); + assertEquals(12L, itinerary.waitingTime); + } else if (type == Type.BACKWARD) { + assertEquals(37L, itinerary.walkTime); + assertEquals(17L, itinerary.transitTime); + assertEquals(14L, itinerary.waitingTime); } else if (type == Type.ONBOARD) { - assertEquals(24L, itinerary.walkTime); - assertEquals(21L, itinerary.transitTime); - assertEquals(9L, itinerary.waitingTime); + assertEquals(30L, itinerary.walkTime); + assertEquals(16L, itinerary.transitTime); + assertEquals(14L, itinerary.waitingTime); } if (type == Type.FORWARD || type == Type.BACKWARD) { @@ -1050,9 +1059,7 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[0].alightRule); assertFalse(legs[0].pathway); assertEquals("WALK", legs[0].mode); - assertEquals(0L, legs[0].startTime.getTimeInMillis()); - assertEquals(3000L, legs[0].endTime.getTimeInMillis()); - assertEquals(3.0, legs[0].getDuration(), 0.0); + assertEquals(7.0, legs[0].getDuration(), 0.0); assertEquals(0, legs[0].departureDelay); assertEquals(0, legs[0].arrivalDelay); assertFalse(legs[0].realTime); @@ -1063,6 +1070,17 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[0]); } + // Forward and backward are different because of slope costs for walking + if (type == Type.FORWARD) { + assertEquals(0L, legs[0].startTime.getTimeInMillis()); + assertEquals(7000L, legs[0].endTime.getTimeInMillis()); + assertEquals(16000L, legs[1].endTime.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(2000L, legs[0].startTime.getTimeInMillis()); + assertEquals(9000L, legs[0].endTime.getTimeInMillis()); + assertEquals(16000L, legs[1].endTime.getTimeInMillis()); + } + assertEquals("Train", legs[1].agencyId); assertEquals("John Train", legs[1].agencyName); assertEquals("http://www.train.org/", legs[1].agencyUrl); @@ -1092,13 +1110,13 @@ private void compareLegs(Leg[] legs, Type type) { assertFalse(legs[1].pathway); assertEquals("RAIL", legs[1].mode); if (type == Type.FORWARD || type == Type.BACKWARD) { - assertEquals(4000L, legs[1].startTime.getTimeInMillis()); - assertEquals(12.0, legs[1].getDuration(), 0.0); + assertEquals(10000L, legs[1].startTime.getTimeInMillis()); + assertEquals(6.0, legs[1].getDuration(), 0.0); } else if (type == Type.ONBOARD) { assertEquals(6000L, legs[1].startTime.getTimeInMillis()); - assertEquals(10.0, legs[1].getDuration(), 0.0); + assertEquals(5.0, legs[1].getDuration(), 0.0); } - assertEquals(16000L, legs[1].endTime.getTimeInMillis()); + assertEquals(0, legs[1].departureDelay); assertEquals(0, legs[1].arrivalDelay); assertFalse(legs[1].realTime); @@ -1109,7 +1127,6 @@ private void compareLegs(Leg[] legs, Type type) { } else if (type == Type.ONBOARD) { assertEquals(O_DISTANCE, legs[1].distance, EPSILON); } - assertEquals("Train", legs[2].agencyId); assertEquals("John Train", legs[2].agencyName); assertEquals("http://www.train.org/", legs[2].agencyUrl); @@ -1134,8 +1151,7 @@ private void compareLegs(Leg[] legs, Type type) { assertEquals("mustPhone", legs[2].alightRule); assertFalse(legs[2].pathway); assertEquals("RAIL", legs[2].mode); - assertEquals(20000L, legs[2].startTime.getTimeInMillis()); - assertEquals(24000L, legs[2].endTime.getTimeInMillis()); + assertEquals(4.0, legs[2].getDuration(), 0.0); assertEquals(0, legs[2].departureDelay); assertEquals(0, legs[2].arrivalDelay); @@ -1168,12 +1184,16 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[3].alightRule); assertFalse(legs[3].pathway); assertEquals("WALK", legs[3].mode); - if (type == Type.FORWARD || type == Type.ONBOARD) { + if (type == Type.FORWARD) { assertEquals(24000L, legs[3].startTime.getTimeInMillis()); assertEquals(32000L, legs[3].endTime.getTimeInMillis()); + assertEquals(20000L, legs[2].startTime.getTimeInMillis()); + assertEquals(24000L, legs[2].endTime.getTimeInMillis()); } else if (type == Type.BACKWARD) { assertEquals(32000L, legs[3].startTime.getTimeInMillis()); assertEquals(40000L, legs[3].endTime.getTimeInMillis()); + assertEquals(20000L, legs[2].startTime.getTimeInMillis()); + assertEquals(24000L, legs[2].endTime.getTimeInMillis()); } assertEquals(8.0, legs[3].getDuration(), 0.0); assertEquals(0, legs[3].departureDelay); @@ -1211,8 +1231,8 @@ private void compareLegs(Leg[] legs, Type type) { assertEquals(40000L, legs[4].startTime.getTimeInMillis()); assertEquals(43000L, legs[4].endTime.getTimeInMillis()); assertEquals(3.0, legs[4].getDuration(), 0.0); - assertEquals(8, legs[4].departureDelay); - assertEquals(7, legs[4].arrivalDelay); + assertEquals(4, legs[4].departureDelay); + assertEquals(5, legs[4].arrivalDelay); assertTrue(legs[4].realTime); assertNull(legs[4].isNonExactFrequency); assertNull(legs[4].headway); @@ -1242,15 +1262,20 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[5].alightRule); assertFalse(legs[5].pathway); assertEquals("WALK", legs[5].mode); - assertEquals(44000L, legs[5].startTime.getTimeInMillis()); - assertEquals(53000L, legs[5].endTime.getTimeInMillis()); - assertEquals(9.0, legs[5].getDuration(), 0.0); + assertEquals(15.0, legs[5].getDuration(), 0.0); assertEquals(0, legs[5].departureDelay); assertEquals(0, legs[5].arrivalDelay); assertFalse(legs[5].realTime); assertNull(legs[5].isNonExactFrequency); assertNull(legs[5].headway); assertEquals(F_DISTANCE[5], legs[5].distance, 0.0); + if (type == Type.FORWARD) { + assertEquals(44000L, legs[5].startTime.getTimeInMillis()); + assertEquals(59000L, legs[5].endTime.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(48000L, legs[5].startTime.getTimeInMillis()); + assertEquals(63000L, legs[5].endTime.getTimeInMillis()); + } assertNull(legs[6].agencyId); assertNull(legs[6].agencyName); @@ -1276,8 +1301,6 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[6].alightRule); assertFalse(legs[6].pathway); assertEquals("BICYCLE", legs[6].mode); - assertEquals(53000L, legs[6].startTime.getTimeInMillis()); - assertEquals(55000L, legs[6].endTime.getTimeInMillis()); assertEquals(2.0, legs[6].getDuration(), 0.0); assertEquals(0, legs[6].departureDelay); assertEquals(0, legs[6].arrivalDelay); @@ -1285,6 +1308,13 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[6].isNonExactFrequency); assertNull(legs[6].headway); assertEquals(F_DISTANCE[6], legs[6].distance, 0.0); + if (type == Type.FORWARD) { + assertEquals(59000L, legs[6].startTime.getTimeInMillis()); + assertEquals(61000L, legs[6].endTime.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(63000L, legs[6].startTime.getTimeInMillis()); + assertEquals(65000L, legs[6].endTime.getTimeInMillis()); + } assertNull(legs[7].agencyId); assertNull(legs[7].agencyName); @@ -1311,8 +1341,6 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[7].alightRule); assertFalse(legs[7].pathway); assertEquals("BICYCLE", legs[7].mode); - assertEquals(55000L, legs[7].startTime.getTimeInMillis()); - assertEquals(57000L, legs[7].endTime.getTimeInMillis()); assertEquals(2.0, legs[7].getDuration(), 0.0); assertEquals(0, legs[7].departureDelay); assertEquals(0, legs[7].arrivalDelay); @@ -1320,6 +1348,13 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[7].isNonExactFrequency); assertNull(legs[7].headway); assertEquals(F_DISTANCE[7], legs[7].distance, 0.0); + if (type == Type.FORWARD) { + assertEquals(61000L, legs[7].startTime.getTimeInMillis()); + assertEquals(63000L, legs[7].endTime.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(65000L, legs[7].startTime.getTimeInMillis()); + assertEquals(67000L, legs[7].endTime.getTimeInMillis()); + } assertNull(legs[8].agencyId); assertNull(legs[8].agencyName); @@ -1345,8 +1380,6 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[8].alightRule); assertFalse(legs[8].pathway); assertEquals("WALK", legs[8].mode); - assertEquals(57000L, legs[8].startTime.getTimeInMillis()); - assertEquals(60000L, legs[8].endTime.getTimeInMillis()); assertEquals(3.0, legs[8].getDuration(), 0.0); assertEquals(0, legs[8].departureDelay); assertEquals(0, legs[8].arrivalDelay); @@ -1354,6 +1387,13 @@ private void compareLegs(Leg[] legs, Type type) { assertNull(legs[8].isNonExactFrequency); assertNull(legs[8].headway); assertEquals(F_DISTANCE[8], legs[8].distance, 0.0); + if (type == Type.FORWARD) { + assertEquals(63000L, legs[8].startTime.getTimeInMillis()); + assertEquals(66000L, legs[8].endTime.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(67000L, legs[8].startTime.getTimeInMillis()); + assertEquals(70000L, legs[8].endTime.getTimeInMillis()); + } } /** Compare all simple walk step fields to their expected values, step by step. */ @@ -1520,7 +1560,6 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[0][0].zoneId); assertNull(places[0][0].orig); assertNull(places[0][0].arrival); - assertEquals(0L, places[0][0].departure.getTimeInMillis()); assertEquals("Train stop depart", places[0][1].name); assertEquals(1, places[0][1].lon, 0.0); @@ -1531,8 +1570,7 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Train depart platform", places[0][1].platformCode); assertEquals("Train depart zone", places[0][1].zoneId); assertNull(places[0][1].orig); - assertEquals(3000L, places[0][1].arrival.getTimeInMillis()); - assertEquals(4000L, places[0][1].departure.getTimeInMillis()); + assertEquals(10000L, places[0][1].departure.getTimeInMillis()); assertEquals("Train stop depart", places[1][0].name); assertEquals(1, places[1][0].lon, 0.0); @@ -1543,8 +1581,7 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Train depart platform", places[1][0].platformCode); assertEquals("Train depart zone", places[1][0].zoneId); assertNull(places[1][0].orig); - assertEquals(3000L, places[1][0].arrival.getTimeInMillis()); - assertEquals(4000L, places[1][0].departure.getTimeInMillis()); + assertEquals(10000L, places[1][0].departure.getTimeInMillis()); } else if (type == Type.ONBOARD) { assertNull(places[0][0]); @@ -1563,6 +1600,16 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals(6000L, places[1][0].departure.getTimeInMillis()); } + if (type == Type.FORWARD) { + assertEquals(0L, places[0][0].departure.getTimeInMillis()); + assertEquals(7000L, places[0][1].arrival.getTimeInMillis()); + assertEquals(7000L, places[1][0].arrival.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(2000L, places[0][0].departure.getTimeInMillis()); + assertEquals(9000L, places[0][1].arrival.getTimeInMillis()); + assertEquals(9000L, places[1][0].arrival.getTimeInMillis()); + } + assertEquals("Train stop dwell", places[1][1].name); assertEquals(45, places[1][1].lon, 0.0); assertEquals(23, places[1][1].lat, 0.0); @@ -1572,8 +1619,6 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Train dwell platform", places[1][1].platformCode); assertEquals("Train dwell zone", places[1][1].zoneId); assertNull(places[1][1].orig); - assertEquals(8000L, places[1][1].arrival.getTimeInMillis()); - assertEquals(12000L, places[1][1].departure.getTimeInMillis()); assertEquals("Train stop interline", places[1][2].name); assertEquals(89, places[1][2].lon, 0.0); @@ -1584,8 +1629,6 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Train interline platform", places[1][2].platformCode); assertEquals("Train interline zone", places[1][2].zoneId); assertNull(places[1][2].orig); - assertEquals(16000L, places[1][2].arrival.getTimeInMillis()); - assertEquals(20000L, places[1][2].departure.getTimeInMillis()); assertEquals("Train stop interline", places[2][0].name); assertEquals(89, places[2][0].lon, 0.0); @@ -1596,8 +1639,6 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Train interline platform", places[2][0].platformCode); assertEquals("Train interline zone", places[2][0].zoneId); assertNull(places[2][0].orig); - assertEquals(16000L, places[2][0].arrival.getTimeInMillis()); - assertEquals(20000L, places[2][0].departure.getTimeInMillis()); assertEquals("Train stop arrive", places[2][1].name); assertEquals(133, places[2][1].lon, 0.0); @@ -1608,11 +1649,24 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Train arrive platform", places[2][1].platformCode); assertEquals("Train arrive zone", places[2][1].zoneId); assertNull(places[2][1].orig); - assertEquals(24000L, places[2][1].arrival.getTimeInMillis()); - if (type == Type.FORWARD || type == Type.ONBOARD) { + if (type == Type.FORWARD) { assertEquals(24000L, places[2][1].departure.getTimeInMillis()); + assertEquals(12000L, places[1][1].arrival.getTimeInMillis()); + assertEquals(13000L, places[1][1].departure.getTimeInMillis()); + assertEquals(16000L, places[1][2].arrival.getTimeInMillis()); + assertEquals(20000L, places[1][2].departure.getTimeInMillis()); + assertEquals(16000L, places[2][0].arrival.getTimeInMillis()); + assertEquals(20000L, places[2][0].departure.getTimeInMillis()); + assertEquals(24000L, places[2][1].arrival.getTimeInMillis()); } else if (type == Type.BACKWARD) { assertEquals(32000L, places[2][1].departure.getTimeInMillis()); + assertEquals(12000L, places[1][1].arrival.getTimeInMillis()); + assertEquals(13000L, places[1][1].departure.getTimeInMillis()); + assertEquals(16000L, places[1][2].arrival.getTimeInMillis()); + assertEquals(20000L, places[1][2].departure.getTimeInMillis()); + assertEquals(16000L, places[2][0].arrival.getTimeInMillis()); + assertEquals(20000L, places[2][0].departure.getTimeInMillis()); + assertEquals(24000L, places[2][1].arrival.getTimeInMillis()); } assertEquals("Train stop arrive", places[3][0].name); @@ -1624,11 +1678,12 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Train arrive platform", places[3][0].platformCode); assertEquals("Train arrive zone", places[3][0].zoneId); assertNull(places[3][0].orig); - assertEquals(24000L, places[3][0].arrival.getTimeInMillis()); - if (type == Type.FORWARD || type == Type.ONBOARD) { + if (type == Type.FORWARD) { assertEquals(24000L, places[3][0].departure.getTimeInMillis()); + assertEquals(24000L, places[3][0].arrival.getTimeInMillis()); } else if (type == Type.BACKWARD) { assertEquals(32000L, places[3][0].departure.getTimeInMillis()); + assertEquals(24000L, places[3][0].arrival.getTimeInMillis()); } assertEquals("Ferry stop depart", places[3][1].name); @@ -1640,7 +1695,7 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Ferry depart platform", places[3][1].platformCode); assertEquals("Ferry depart zone", places[3][1].zoneId); assertNull(places[3][1].orig); - if (type == Type.FORWARD || type == Type.ONBOARD) { + if (type == Type.FORWARD) { assertEquals(32000L, places[3][1].arrival.getTimeInMillis()); } else if (type == Type.BACKWARD) { assertEquals(40000L, places[3][1].arrival.getTimeInMillis()); @@ -1656,7 +1711,7 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Ferry depart platform", places[4][0].platformCode); assertEquals("Ferry depart zone", places[4][0].zoneId); assertNull(places[4][0].orig); - if (type == Type.FORWARD || type == Type.ONBOARD) { + if (type == Type.FORWARD) { assertEquals(32000L, places[4][0].arrival.getTimeInMillis()); } else if (type == Type.BACKWARD) { assertEquals(40000L, places[4][0].arrival.getTimeInMillis()); @@ -1672,8 +1727,13 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Ferry arrive platform", places[4][1].platformCode); assertEquals("Ferry arrive zone", places[4][1].zoneId); assertNull(places[4][1].orig); - assertEquals(43000L, places[4][1].arrival.getTimeInMillis()); - assertEquals(44000L, places[4][1].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(43000L, places[4][1].arrival.getTimeInMillis()); + assertEquals(44000L, places[4][1].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(43000L, places[4][1].arrival.getTimeInMillis()); + assertEquals(48000L, places[4][1].departure.getTimeInMillis()); + } assertEquals("Ferry stop arrive", places[5][0].name); assertEquals(179, places[5][0].lon, 0.0); @@ -1684,8 +1744,14 @@ private void comparePlaces(Place[][] places, Type type) { assertEquals("Ferry arrive platform", places[5][0].platformCode); assertEquals("Ferry arrive zone", places[5][0].zoneId); assertNull(places[5][0].orig); - assertEquals(43000L, places[5][0].arrival.getTimeInMillis()); - assertEquals(44000L, places[5][0].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(43000L, places[5][0].arrival.getTimeInMillis()); + assertEquals(44000L, places[5][0].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(43000L, places[5][0].arrival.getTimeInMillis()); + assertEquals(48000L, places[5][0].departure.getTimeInMillis()); + } + assertEquals("Exit pickup station", places[5][1].name); assertEquals(180, places[5][1].lon, 0.0); @@ -1696,8 +1762,13 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[5][1].platformCode); assertNull(places[5][1].zoneId); assertNull(places[5][1].orig); - assertEquals(53000L, places[5][1].arrival.getTimeInMillis()); - assertEquals(53000L, places[5][1].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(59000L, places[5][1].arrival.getTimeInMillis()); + assertEquals(59000L, places[5][1].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(63000L, places[5][1].arrival.getTimeInMillis()); + assertEquals(63000L, places[5][1].departure.getTimeInMillis()); + } assertEquals("Exit pickup station", places[6][0].name); assertEquals(180, places[6][0].lon, 0.0); @@ -1708,8 +1779,13 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[6][0].platformCode); assertNull(places[6][0].zoneId); assertNull(places[6][0].orig); - assertEquals(53000L, places[6][0].arrival.getTimeInMillis()); - assertEquals(53000L, places[6][0].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(59000L, places[6][0].arrival.getTimeInMillis()); + assertEquals(59000L, places[6][0].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(63000L, places[6][0].arrival.getTimeInMillis()); + assertEquals(63000L, places[6][0].departure.getTimeInMillis()); + } assertEquals(90, places[6][1].lon, 0.0); assertEquals(90, places[6][1].lat, 0.0); @@ -1719,8 +1795,13 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[6][1].platformCode); assertNull(places[6][1].zoneId); assertNull(places[6][1].orig); - assertEquals(55000L, places[6][1].arrival.getTimeInMillis()); - assertEquals(55000L, places[6][1].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(61000L, places[6][1].arrival.getTimeInMillis()); + assertEquals(61000L, places[6][1].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(65000L, places[6][1].arrival.getTimeInMillis()); + assertEquals(65000L, places[6][1].departure.getTimeInMillis()); + } assertEquals(90, places[7][0].lon, 0.0); assertEquals(90, places[7][0].lat, 0.0); @@ -1730,8 +1811,13 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[7][0].platformCode); assertNull(places[7][0].zoneId); assertNull(places[7][0].orig); - assertEquals(55000L, places[7][0].arrival.getTimeInMillis()); - assertEquals(55000L, places[7][0].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(61000L, places[7][0].arrival.getTimeInMillis()); + assertEquals(61000L, places[7][0].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(65000L, places[7][0].arrival.getTimeInMillis()); + assertEquals(65000L, places[7][0].departure.getTimeInMillis()); + } assertEquals("Enter dropoff station", places[7][1].name); assertEquals(0, places[7][1].lon, 0.0); @@ -1742,8 +1828,13 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[7][1].platformCode); assertNull(places[7][1].zoneId); assertNull(places[7][1].orig); - assertEquals(57000L, places[7][1].arrival.getTimeInMillis()); - assertEquals(57000L, places[7][1].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(63000L, places[7][1].arrival.getTimeInMillis()); + assertEquals(63000L, places[7][1].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(67000L, places[7][1].arrival.getTimeInMillis()); + assertEquals(67000L, places[7][1].departure.getTimeInMillis()); + } assertEquals("Enter dropoff station", places[8][0].name); assertEquals(0, places[8][0].lon, 0.0); @@ -1754,8 +1845,13 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[8][0].platformCode); assertNull(places[8][0].zoneId); assertNull(places[8][0].orig); - assertEquals(57000L, places[8][0].arrival.getTimeInMillis()); - assertEquals(57000L, places[8][0].departure.getTimeInMillis()); + if (type == Type.FORWARD) { + assertEquals(63000L, places[8][0].arrival.getTimeInMillis()); + assertEquals(63000L, places[8][0].departure.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(67000L, places[8][0].arrival.getTimeInMillis()); + assertEquals(67000L, places[8][0].departure.getTimeInMillis()); + } assertEquals(0, places[8][1].lon, 0.0); assertEquals(90, places[8][1].lat, 0.0); @@ -1765,8 +1861,12 @@ private void comparePlaces(Place[][] places, Type type) { assertNull(places[8][1].platformCode); assertNull(places[8][1].zoneId); assertNull(places[8][1].orig); - assertEquals(60000L, places[8][1].arrival.getTimeInMillis()); assertNull(places[8][1].departure); + if (type == Type.FORWARD) { + assertEquals(66000L, places[8][1].arrival.getTimeInMillis()); + } else if (type == Type.BACKWARD) { + assertEquals(70000L, places[8][1].arrival.getTimeInMillis()); + } } /** Compare the stop ids to their expected values, place by place. */ diff --git a/src/test/java/org/opentripplanner/common/geometry/CompactElevationProfileTest.java b/src/test/java/org/opentripplanner/common/geometry/CompactElevationProfileTest.java index 21d6885492d..9ef8ba00632 100644 --- a/src/test/java/org/opentripplanner/common/geometry/CompactElevationProfileTest.java +++ b/src/test/java/org/opentripplanner/common/geometry/CompactElevationProfileTest.java @@ -2,8 +2,8 @@ import junit.framework.TestCase; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; public class CompactElevationProfileTest extends TestCase { diff --git a/src/test/java/org/opentripplanner/common/geometry/CompactLineStringTest.java b/src/test/java/org/opentripplanner/common/geometry/CompactLineStringTest.java index bba8918e8f5..62a4045c585 100644 --- a/src/test/java/org/opentripplanner/common/geometry/CompactLineStringTest.java +++ b/src/test/java/org/opentripplanner/common/geometry/CompactLineStringTest.java @@ -8,9 +8,9 @@ import org.junit.Test; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; public class CompactLineStringTest extends TestCase { diff --git a/src/test/java/org/opentripplanner/common/geometry/DirectionUtilsTest.java b/src/test/java/org/opentripplanner/common/geometry/DirectionUtilsTest.java index a0c1b71d1df..966d13ca400 100644 --- a/src/test/java/org/opentripplanner/common/geometry/DirectionUtilsTest.java +++ b/src/test/java/org/opentripplanner/common/geometry/DirectionUtilsTest.java @@ -9,7 +9,7 @@ import org.geotools.referencing.GeodeticCalculator; import org.junit.Test; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; public class DirectionUtilsTest extends TestCase { diff --git a/src/test/java/org/opentripplanner/common/geometry/GeoJsonTest.java b/src/test/java/org/opentripplanner/common/geometry/GeoJsonTest.java new file mode 100644 index 00000000000..dbbf493e8de --- /dev/null +++ b/src/test/java/org/opentripplanner/common/geometry/GeoJsonTest.java @@ -0,0 +1,157 @@ +package org.opentripplanner.common.geometry; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class GeoJsonTest { + private GeometryFactory gf = new GeometryFactory(); + private ObjectWriter writer; + private ObjectMapper mapper; + + @Before + public void setupMapper() { + + mapper = new ObjectMapper(); + mapper.registerModule(new GeoJsonModule()); + + writer = mapper.writer(); + } + + @Test + public void point() throws Exception { + Point point = gf.createPoint(new Coordinate(1.2345678, 2.3456789)); + assertRoundTrip(point); + assertThat( + toJson(point), + is("{\"type\":\"Point\",\"coordinates\":[1.2345678,2.3456789]}")); + } + + private String toJson(Object value) throws JsonGenerationException, + JsonMappingException, IOException { + return writer.writeValueAsString(value); + } + + @Test + public void multiPoint() throws Exception { + MultiPoint multiPoint = gf.createMultiPoint(new Point[] { gf + .createPoint(new Coordinate(1.2345678, 2.3456789)) }); + assertRoundTrip(multiPoint); + assertThat( + toJson(multiPoint), + is("{\"type\":\"MultiPoint\",\"coordinates\":[[1.2345678,2.3456789]]}")); + } + + @Test + public void lineString() throws Exception { + LineString lineString = gf.createLineString(new Coordinate[] { + new Coordinate(100.0, 0.0), new Coordinate(101.0, 1.0) }); + assertRoundTrip(lineString); + assertThat( + toJson(lineString), + is("{\"type\":\"LineString\",\"coordinates\":[[100.0,0.0],[101.0,1.0]]}")); + } + + @Test + public void multiLineString() throws Exception { + + MultiLineString multiLineString = gf + .createMultiLineString(new LineString[] { + gf.createLineString(new Coordinate[] { + new Coordinate(100.0, 0.0), + new Coordinate(101.0, 1.0) }), + gf.createLineString(new Coordinate[] { + new Coordinate(102.0, 2.0), + new Coordinate(103.0, 3.0) }) }); + + assertRoundTrip(multiLineString); + assertThat( + toJson(multiLineString), + is("{\"type\":\"MultiLineString\",\"coordinates\":[[[100.0,0.0],[101.0,1.0]],[[102.0,2.0],[103.0,3.0]]]}")); + } + + @Test + public void polygon() throws Exception { + LinearRing shell = gf.createLinearRing(new Coordinate[] { + new Coordinate(102.0, 2.0), new Coordinate(103.0, 2.0), + new Coordinate(103.0, 3.0), new Coordinate(102.0, 3.0), + new Coordinate(102.0, 2.0) }); + LinearRing[] holes = new LinearRing[0]; + Polygon polygon = gf.createPolygon(shell, holes); + + assertRoundTrip(polygon); + assertThat( + toJson(polygon), + is("{\"type\":\"Polygon\",\"coordinates\":[[[102.0,2.0],[103.0,2.0],[103.0,3.0],[102.0,3.0],[102.0,2.0]]]}")); + } + + @Test + public void polygonWithNoHoles() throws Exception { + LinearRing shell = gf.createLinearRing(new Coordinate[] { + new Coordinate(102.0, 2.0), new Coordinate(103.0, 2.0), + new Coordinate(103.0, 3.0), new Coordinate(102.0, 3.0), + new Coordinate(102.0, 2.0) }); + LinearRing[] holes = new LinearRing[] { gf + .createLinearRing(new Coordinate[] { + new Coordinate(100.2, 0.2), new Coordinate(100.8, 0.2), + new Coordinate(100.8, 0.8), new Coordinate(100.2, 0.8), + new Coordinate(100.2, 0.2) }) }; + assertThat( + toJson(gf.createPolygon(shell, holes)), + is("{\"type\":\"Polygon\",\"coordinates\":[[[102.0,2.0],[103.0,2.0],[103.0,3.0],[102.0,3.0],[102.0,2.0]],[[100.2,0.2],[100.8,0.2],[100.8,0.8],[100.2,0.8],[100.2,0.2]]]}")); + } + + @Test + public void multiPolygon() throws Exception { + LinearRing shell = gf.createLinearRing(new Coordinate[] { + new Coordinate(102.0, 2.0), new Coordinate(103.0, 2.0), + new Coordinate(103.0, 3.0), new Coordinate(102.0, 3.0), + new Coordinate(102.0, 2.0) }); + MultiPolygon multiPolygon = gf.createMultiPolygon(new Polygon[] { gf + .createPolygon(shell, null) }); + + assertRoundTrip(multiPolygon); + assertThat( + toJson(multiPolygon), + is("{\"type\":\"MultiPolygon\",\"coordinates\":[[[[102.0,2.0],[103.0,2.0],[103.0,3.0],[102.0,3.0],[102.0,2.0]]]]}")); + } + + @Test + public void geometryCollection() throws Exception { + GeometryCollection collection = gf + .createGeometryCollection(new Geometry[] { gf + .createPoint(new Coordinate(1.2345678, 2.3456789)) }); + assertRoundTrip(collection); + assertThat( + toJson(collection), + is("{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"Point\",\"coordinates\":[1.2345678,2.3456789]}]}")); + } + + private void assertRoundTrip(Geometry geom) throws JsonGenerationException, + JsonMappingException, IOException { + String json = writer.writeValueAsString(geom); + Geometry regeom = mapper.reader(Geometry.class).readValue(json); + assertThat(regeom, equalTo(geom)); + } + +} diff --git a/src/test/java/org/opentripplanner/common/geometry/GeometryUtilsTest.java b/src/test/java/org/opentripplanner/common/geometry/GeometryUtilsTest.java index cbb48648df9..56b15207736 100644 --- a/src/test/java/org/opentripplanner/common/geometry/GeometryUtilsTest.java +++ b/src/test/java/org/opentripplanner/common/geometry/GeometryUtilsTest.java @@ -1,17 +1,54 @@ package org.opentripplanner.common.geometry; -import static org.junit.Assert.assertEquals; - import org.junit.Test; import org.opentripplanner.common.model.P2; +import org.opentripplanner.analyst.UnsupportedGeometryException; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequenceFactory; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.LineString; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.CoordinateSequenceFactory; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class GeometryUtilsTest { + + private static final double tolerance = 0.000001; + + @Test + public final void testConvertGeoJsonToJtsGeometry() + throws UnsupportedGeometryException { + + { // Should convert Point correctly + double lng1 = -77.1111; + double lat1 = 38.1111; + org.geojson.Point p1 = new org.geojson.Point(lng1, lat1); + Point geometry = (Point) GeometryUtils.convertGeoJsonToJtsGeometry(p1); + assertEquals(lng1, geometry.getX(), tolerance); + assertEquals(lat1, geometry.getY(), tolerance); + } + + { // Should convert LineString correctly + double lng1 = -77.1111; + double lat1 = 38.1111; + double lng2 = -77.2222; + double lat2 = 38.2222; + org.geojson.LngLatAlt a1 = new org.geojson.LngLatAlt(lng1, lat1); + org.geojson.LngLatAlt a2 = new org.geojson.LngLatAlt(lng2, lat2); + org.geojson.LineString lineString = new org.geojson.LineString(a1, a2); + LineString geometry = (LineString) GeometryUtils.convertGeoJsonToJtsGeometry(lineString); + assertEquals(lng1, geometry.getCoordinateN(0).x, tolerance); + assertEquals(lat1, geometry.getCoordinateN(0).y, tolerance); + assertEquals(lng2, geometry.getCoordinateN(1).x, tolerance); + assertEquals(lat2, geometry.getCoordinateN(1).y, tolerance); + } + } + @Test public final void testSplitGeometryAtFraction() { Coordinate[] coordinates = new Coordinate[4]; @@ -205,4 +242,21 @@ public final void testSplitGeometryAtFraction() { geometry = new LineString(sequence, geometryFactory); assertEquals(geometry, results[8][1]); } + + @Test + public void testWkt() { + String wkt = "POLYGON((1.0 2.0,3.0 -4.0,-1.0 2.0, 1.0 2.0))"; + Geometry geom = GeometryUtils.parseWkt(wkt); + assertTrue(geom instanceof Polygon); + Coordinate[] coords = geom.getCoordinates(); + assertEquals(4, coords.length); + assertEquals(1.0, coords[0].getOrdinate(0), tolerance); + assertEquals(2.0, coords[0].getOrdinate(1), tolerance); + assertEquals(3.0, coords[1].getOrdinate(0), tolerance); + assertEquals(-4.0, coords[1].getOrdinate(1), tolerance); + assertEquals(-1.0, coords[2].getOrdinate(0), tolerance); + assertEquals(2.0, coords[2].getOrdinate(1), tolerance); + assertEquals(1.0, coords[3].getOrdinate(0), tolerance); + assertEquals(2.0, coords[3].getOrdinate(1), tolerance); + } } diff --git a/src/test/java/org/opentripplanner/common/geometry/HashGridTest.java b/src/test/java/org/opentripplanner/common/geometry/HashGridTest.java index d988707a860..f665f7327dd 100644 --- a/src/test/java/org/opentripplanner/common/geometry/HashGridTest.java +++ b/src/test/java/org/opentripplanner/common/geometry/HashGridTest.java @@ -7,10 +7,10 @@ import junit.framework.TestCase; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.index.SpatialIndex; -import com.vividsolutions.jts.index.strtree.STRtree; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.SpatialIndex; +import org.locationtech.jts.index.strtree.STRtree; public class HashGridTest extends TestCase { diff --git a/src/test/java/org/opentripplanner/common/geometry/TestDistanceLib.java b/src/test/java/org/opentripplanner/common/geometry/TestDistanceLib.java index 85467e7768e..df546104dfd 100644 --- a/src/test/java/org/opentripplanner/common/geometry/TestDistanceLib.java +++ b/src/test/java/org/opentripplanner/common/geometry/TestDistanceLib.java @@ -1,8 +1,8 @@ package org.opentripplanner.common.geometry; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import junit.framework.TestCase; diff --git a/src/test/java/org/opentripplanner/common/model/GenericLocationTest.java b/src/test/java/org/opentripplanner/common/model/GenericLocationTest.java index ac9f28ee56f..1e437c6124b 100644 --- a/src/test/java/org/opentripplanner/common/model/GenericLocationTest.java +++ b/src/test/java/org/opentripplanner/common/model/GenericLocationTest.java @@ -3,7 +3,7 @@ import static org.junit.Assert.*; import org.junit.Test; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; public class GenericLocationTest { diff --git a/src/test/java/org/opentripplanner/geocoder/bano/BanoGeocoderTest.java b/src/test/java/org/opentripplanner/geocoder/bano/BanoGeocoderTest.java index 46bbea52fa0..e088a3583c1 100644 --- a/src/test/java/org/opentripplanner/geocoder/bano/BanoGeocoderTest.java +++ b/src/test/java/org/opentripplanner/geocoder/bano/BanoGeocoderTest.java @@ -1,54 +1,46 @@ package org.opentripplanner.geocoder.bano; -import org.junit.Assume; import org.junit.Test; +import org.junit.Ignore; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.geocoder.GeocoderResult; import org.opentripplanner.geocoder.GeocoderResults; -import com.vividsolutions.jts.geom.Envelope; -import java.io.IOException; -import java.net.URL; -import java.net.UnknownHostException; +import org.locationtech.jts.geom.Envelope; public class BanoGeocoderTest { /** + * NOTE! THIS TEST RELAY ON AN ON-LINE EXTERNAL API (Bano Geocoder) TO BE UP AN RUNNING, WHICH + * MAY NOT BE THE CASE. HENCE THE '@Ignore'. + *

* TODO -- This unit-test rely on an on-line API to be up and running, which may not be the case * if a network connection is not active or the server is down. */ @Test - public void testOnLine() throws IOException { - assumeConnectedToInternet(); - -// commenting out due to some 502 error responses from the Bano Geocoder instance. -// BanoGeocoder banoGeocoder = new BanoGeocoder(); -// // The Presidential palace of the French Republic is not supposed to move often -// Envelope bbox = new Envelope(); -// bbox.expandToInclude(2.25, 48.8); -// bbox.expandToInclude(2.35, 48.9); -// GeocoderResults results = banoGeocoder.geocode("55 Rue du Faubourg Saint-Honoré", bbox); -// -// assert (results.getResults().size() >= 1); -// -// boolean found = false; -// for (GeocoderResult result : results.getResults()) { -// if (result.getDescription().startsWith("55 Rue du Faubourg")) { -// double dist = SphericalDistanceLibrary.distance(result.getLat(), -// result.getLng(), 48.870637, 2.316939); -// assert (dist < 100); -// found = true; -// } -// } -// assert (found); + @Ignore + public void testOnLine() throws Exception { - } + BanoGeocoder banoGeocoder = new BanoGeocoder(); + // The Presidential palace of the French Republic is not supposed to move often + Envelope bbox = new Envelope(); + bbox.expandToInclude(2.25, 48.8); + bbox.expandToInclude(2.35, 48.9); + GeocoderResults results = banoGeocoder.geocode("55 Rue du Faubourg Saint-Honoré", bbox); + + assert (results.getResults().size() >= 1); - private static void assumeConnectedToInternet() throws IOException { - try { - new URL("http://www.google.com").openConnection().connect(); - } catch (UnknownHostException e) { - Assume.assumeTrue("Skips tests if not on internet.", false); + boolean found = false; + for (GeocoderResult result : results.getResults()) { + if (result.getDescription().contains("55 Rue du Faubourg")) { + double dist = SphericalDistanceLibrary.distance(result.getLat(), + result.getLng(), 48.870637, 2.316939); + assert (dist < 100); + found = true; + } } + assert (found); + } -} + +} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/geocoder/ws/GeocoderServerTest.java b/src/test/java/org/opentripplanner/geocoder/ws/GeocoderServerTest.java index fcd78ea34a8..1dfeacaee32 100644 --- a/src/test/java/org/opentripplanner/geocoder/ws/GeocoderServerTest.java +++ b/src/test/java/org/opentripplanner/geocoder/ws/GeocoderServerTest.java @@ -14,7 +14,7 @@ import org.opentripplanner.geocoder.GeocoderResult; import org.opentripplanner.geocoder.GeocoderResults; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Envelope; public class GeocoderServerTest { diff --git a/src/test/java/org/opentripplanner/graph_builder/linking/LinkStopToPlatformTest.java b/src/test/java/org/opentripplanner/graph_builder/linking/LinkStopToPlatformTest.java index c21340147db..f82134f04e9 100644 --- a/src/test/java/org/opentripplanner/graph_builder/linking/LinkStopToPlatformTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/linking/LinkStopToPlatformTest.java @@ -1,9 +1,9 @@ package org.opentripplanner.graph_builder.linking; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.junit.Before; import org.junit.Test; import org.opentripplanner.common.geometry.GeometryUtils; diff --git a/src/test/java/org/opentripplanner/graph_builder/linking/StreetSplitterTest.java b/src/test/java/org/opentripplanner/graph_builder/linking/StreetSplitterTest.java index f25ae2539f3..a2d49ecc5d1 100644 --- a/src/test/java/org/opentripplanner/graph_builder/linking/StreetSplitterTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/linking/StreetSplitterTest.java @@ -1,10 +1,10 @@ package org.opentripplanner.graph_builder.linking; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; import org.junit.Before; import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.common.model.GenericLocation; diff --git a/src/test/java/org/opentripplanner/graph_builder/module/ElevationModuleTest.java b/src/test/java/org/opentripplanner/graph_builder/module/ElevationModuleTest.java index 1cbf3d8e53c..dee56a8abf4 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/ElevationModuleTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/ElevationModuleTest.java @@ -1,9 +1,9 @@ package org.opentripplanner.graph_builder.module; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; import org.junit.Ignore; import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.graph_builder.module.ned.DegreeGridNEDTileSource; diff --git a/src/test/java/org/opentripplanner/graph_builder/module/FakeGraph.java b/src/test/java/org/opentripplanner/graph_builder/module/FakeGraph.java index 3e7e708303d..8c580e55107 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/FakeGraph.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/FakeGraph.java @@ -1,55 +1,34 @@ package org.opentripplanner.graph_builder.module; -import com.conveyal.gtfs.GTFSFeed; -import com.conveyal.gtfs.model.*; -import org.mapdb.Fun; +import org.opentripplanner.graph_builder.linking.StreetSplitter; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.Stop; -import org.opentripplanner.graph_builder.linking.StreetSplitter; import org.opentripplanner.graph_builder.model.GtfsBundle; import org.opentripplanner.graph_builder.module.osm.DefaultWayPropertySetSource; import org.opentripplanner.graph_builder.module.osm.OpenStreetMapModule; -import org.opentripplanner.model.FeedScopedId; -import org.opentripplanner.model.Stop; import org.opentripplanner.openstreetmap.impl.AnyFileBasedOpenStreetMapProviderImpl; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.vertextype.TransitStop; import java.io.File; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; import java.net.URL; -import java.net.URLDecoder; import java.util.Arrays; import java.util.HashMap; -import java.util.Map; /** - * Get fake graphs. TODO clarify what a "fake graph" is and why we are getting them (apparently for testing). - * This seems to be the only place in OTP that we use GTFS-lib, which just happens to be pulled in transitively from - * R5. It's questionable whether we should be manually building up GTFS in code like this. I guess it would be a good - * idea if there were a few more abstractions in use. + * Get graphs of Columbus Ohio with real OSM streets and a synthetic transit system for use in testing. */ public class FakeGraph { - /** Frequency of trips generated by addTransit, seconds */ - public static final int FREQUENCY = 600; - - /** Travel time between stops of trips generated by addTransit, seconds */ - public static final int TRAVEL_TIME = 500; - /** Build a graph in Columbus, OH with no transit */ - public static Graph buildGraphNoTransit () throws UnsupportedEncodingException { + public static Graph buildGraphNoTransit () { Graph gg = new Graph(); OpenStreetMapModule loader = new OpenStreetMapModule(); loader.setDefaultWayPropertySetSource(new DefaultWayPropertySetSource()); AnyFileBasedOpenStreetMapProviderImpl provider = new AnyFileBasedOpenStreetMapProviderImpl(); - File file = new File( - URLDecoder.decode(FakeGraph.class.getResource("columbus.osm.pbf").getFile(), - "UTF-8")); - + File file = getFileForResource("columbus.osm.pbf"); provider.setPath(file); loader.setProvider(provider); @@ -57,339 +36,27 @@ public static Graph buildGraphNoTransit () throws UnsupportedEncodingException { return gg; } - /** Add transit (not just stops) to a Columbus graph */ - public static void addTransit (Graph gg) throws Exception { - // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later - // once we're using the conveyal GTFS lib for everything we ought to be able to do this - // without even writing out the GTFS to a file. - GTFSFeed feed = new GTFSFeed(); - Agency a = createDummyAgency("agency", "Agency", "America/New_York"); - feed.agency.put("agency", a); - - Route r = new Route(); - r.route_short_name = "1"; - r.route_long_name = "High Street"; - r.route_type = 3; - r.agency_id = a.agency_id; - r.route_id = "route"; - feed.routes.put(r.route_id, r); - - Service s = createDummyService(); - feed.services.put(s.service_id, s); - - com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop(); - s1.stop_id = s1.stop_name = "s1"; - s1.stop_lat = 40.2182; - s1.stop_lon = -83.0889; - feed.stops.put(s1.stop_id, s1); - - com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop(); - s2.stop_id = s2.stop_name = "s2"; - s2.stop_lat = 39.9621; - s2.stop_lon = -83.0007; - feed.stops.put(s2.stop_id, s2); - - // make timetabled trips - for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) { - Trip t = new Trip(); - t.trip_id = "trip" + departure; - t.service_id = s.service_id; - t.route_id = r.route_id; - feed.trips.put(t.trip_id, t); - - StopTime st1 = new StopTime(); - st1.trip_id = t.trip_id; - st1.arrival_time = departure; - st1.departure_time = departure; - st1.stop_id = s1.stop_id; - st1.stop_sequence = 1; - feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1); - - StopTime st2 = new StopTime(); - st2.trip_id = t.trip_id; - st2.arrival_time = departure + TRAVEL_TIME; - st2.departure_time = departure + TRAVEL_TIME; - st2.stop_sequence = 2; - st2.stop_id = s2.stop_id; - feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2); - } - - File tempFile = File.createTempFile("gtfs", ".zip"); - feed.toFile(tempFile.getAbsolutePath()); - - // phew. load it into the graph. - GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile))); - gtfs.buildGraph(gg, new GraphBuilderModuleSummary(gtfs)); + public static File getFileForResource(String resource) { + URL resourceUrl = FakeGraph.class.getResource(resource); + return new File(resourceUrl.getPath()); } - /** Add many transit lines to a lot of stops */ - public static void addTransitMultipleLines (Graph g) throws Exception { - // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later - // once we're using the conveyal GTFS lib for everything we ought to be able to do this - // without even writing out the GTFS to a file. - GTFSFeed feed = new GTFSFeed(); - Agency a = createDummyAgency("agency", "Agency", "America/New_York"); - feed.agency.put("agency", a); - - Route r = new Route(); - r.route_short_name = "1"; - r.route_long_name = "High Street"; - r.route_type = 3; - r.agency_id = a.agency_id; - r.route_id = "route"; - feed.routes.put(r.route_id, r); - - Service s = createDummyService(); - feed.services.put(s.service_id, s); - - int stopIdx = 0; - while (stopIdx < 10000) { - com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop(); - s1.stop_id = s1.stop_name = "s" + stopIdx++; - s1.stop_lat = 39.9354 + (stopIdx % 100) * 1e-3; - s1.stop_lon = -83.0589 + (stopIdx / 100) * 1e-3; - feed.stops.put(s1.stop_id, s1); - - com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop(); - s2.stop_id = s2.stop_name = "s" + stopIdx++; - s2.stop_lat = 39.9354 + (stopIdx % 100) * 1e-3; - s2.stop_lon = -83.0589 + (stopIdx / 100) * 1e-3; - feed.stops.put(s2.stop_id, s2); - - // make timetabled trips - int departure = 8 * 3600; - Trip t = new Trip(); - t.trip_id = "trip" + departure + "_" + stopIdx; - t.service_id = s.service_id; - t.route_id = r.route_id; - feed.trips.put(t.trip_id, t); - - StopTime st1 = new StopTime(); - st1.trip_id = t.trip_id; - st1.arrival_time = departure; - st1.departure_time = departure; - st1.stop_id = s1.stop_id; - st1.stop_sequence = 1; - feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1); - - StopTime st2 = new StopTime(); - st2.trip_id = t.trip_id; - st2.arrival_time = departure + 500; - st2.departure_time = departure + 500; - st2.stop_sequence = 2; - st2.stop_id = s2.stop_id; - feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2); - - } - - File tempFile = File.createTempFile("gtfs", ".zip"); - feed.toFile(tempFile.getAbsolutePath()); - - // phew. load it into the graph. - GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile))); + /** + * Add many transit lines to a lot of stops. This is only used by InitialStopsTest. + */ + public static void addTransitMultipleLines (Graph g) { + GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(getFileForResource("addTransitMultipleLines.gtfs.zip")))); gtfs.buildGraph(g, new GraphBuilderModuleSummary(gtfs)); } - public static void addPerpendicularRoutes(Graph graph) throws Exception { - GTFSFeed feed = new GTFSFeed(); - Agency agencyA = createDummyAgency("agencyA", "Agency A", "America/New_York"); - feed.agency.put("agencyA", agencyA); - Agency agencyB = createDummyAgency("agencyB", "Agency B", "America/New_York"); - feed.agency.put("agencyB", agencyB); - Service s = createDummyService(); - feed.services.put(s.service_id, s); - - int stopIdX = 0; - int stopIdY = 0; - for (double lat = 39.9058; lat < 40.0281; lat += 0.005) { - stopIdX = 0; - for (double lon = -83.1341; lon < -82.8646; lon += 0.005) { - com.conveyal.gtfs.model.Stop stop = new com.conveyal.gtfs.model.Stop(); - stop.stop_id = stop.stop_name = String.format("s-%d-%d", stopIdX, stopIdY); - stop.stop_lat = lat; - stop.stop_lon = lon; - feed.stops.put(stop.stop_id, stop); - stopIdX++; - } - stopIdY++; - } - - for (int i = 0; i < stopIdY; i++) { - Route route = new Route(); - route.route_short_name = "hr" + i; - route.route_long_name = i + "th Horizontal Street"; - route.route_type = Route.BUS; - route.agency_id = agencyA.agency_id; - route.route_id = "horizontalroute" + i; - feed.routes.put(route.route_id, route); - } - for (int i = 0; i < stopIdX; i++) { - Route route = new Route(); - route.route_short_name = "vr" + i; - route.route_long_name = i + "th Vertical Street"; - route.route_type = Route.TRAM; - route.agency_id = agencyB.agency_id; - route.route_id = "verticalroute" + i; - feed.routes.put(route.route_id, route); - } - - Map routes = feed.routes; - com.conveyal.gtfs.model.Stop stop; - for (Route route : routes.values()) { - int routeId = Integer.parseInt(route.route_short_name.substring(2)); - int x, y; - boolean isHorizontal = route.route_short_name.startsWith("hr"); - for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) { - Trip t = new Trip(); - t.trip_id = "trip:" + route.route_id + ":" + departure; - t.service_id = s.service_id; - t.route_id = route.route_id; - feed.trips.put(t.trip_id, t); - - int departureTime = departure; - int nrOfStops = (isHorizontal ? stopIdX : stopIdY); - for (int stopSequenceNr = 0; stopSequenceNr < nrOfStops; stopSequenceNr++) { - x = (isHorizontal ? stopSequenceNr : routeId); - y = (isHorizontal ? routeId : stopSequenceNr); - stop = feed.stops.get(String.format("s-%d-%d", x, y)); - StopTime st1 = new StopTime(); - st1.trip_id = t.trip_id; - st1.arrival_time = departureTime; - st1.departure_time = departureTime; - st1.stop_id = stop.stop_id; - st1.stop_sequence = stopSequenceNr + 1; - feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1); - //connect last stop to first so graph is fully reachable - if (stopSequenceNr == 0) { - StopTime stopTime = new StopTime(); - stopTime.trip_id = t.trip_id; - stopTime.arrival_time = departureTime + nrOfStops * 120 + 30 * 60; - stopTime.departure_time = departureTime + nrOfStops * 120 + 30 * 60; - stopTime.stop_id = stop.stop_id; - stopTime.stop_sequence = stopSequenceNr + 1 + nrOfStops; - feed.stop_times.put(new Fun.Tuple2(stopTime.trip_id, stopTime.stop_sequence), stopTime); - } - departureTime += 120; - } - } - } - File tempFile = File.createTempFile("gtfs", ".zip"); - feed.toFile(tempFile.getAbsolutePath()); - - // phew. load it into the graph. - GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile))); + /** + * This introduces a 1MB test resource but is only used by TestIntermediatePlaces. + */ + public static void addPerpendicularRoutes (Graph graph) { + GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(getFileForResource("addPerpendicularRoutes.gtfs.zip")))); gtfs.buildGraph(graph, new GraphBuilderModuleSummary(gtfs)); } - private static Service createDummyService() { - Service s = new Service("service"); - s.calendar = new Calendar(); - s.calendar.service_id = s.service_id; - s.calendar.monday = s.calendar.tuesday = s.calendar.wednesday = s.calendar.thursday = s.calendar.friday = - s.calendar.saturday = s.calendar.sunday = 1; - s.calendar.start_date = 19991231; - s.calendar.end_date = 21001231; - return s; - } - - private static Agency createDummyAgency(String id, String name, String timeZone) { - Agency a = new Agency(); - a.agency_id = id; - a.agency_name = name; - a.agency_timezone = timeZone; - try { - a.agency_url = new URL("http://www.example.com/" + id); - } catch (MalformedURLException e) { - // This really can't happen - assert false; - a.agency_url = null; - } - return a; - } - - /** Add a transit line with multiple patterns to a Columbus graph. Most trips serve stops s1, s2, s3 but some serve only s1, s3 */ - public static void addMultiplePatterns (Graph gg) throws Exception { - // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later - // once we're using the conveyal GTFS lib for everything we ought to be able to do this - // without even writing out the GTFS to a file. - GTFSFeed feed = new GTFSFeed(); - Agency a = createDummyAgency("agency", "Agency", "America/New_York"); - feed.agency.put("agency", a); - - Route r = new Route(); - r.route_short_name = "1"; - r.route_long_name = "High Street"; - r.route_type = 3; - r.agency_id = a.agency_id; - r.route_id = "route"; - feed.routes.put(r.route_id, r); - - Service s = createDummyService(); - feed.services.put(s.service_id, s); - - com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop(); - s1.stop_id = s1.stop_name = "s1"; - s1.stop_lat = 40.2182; - s1.stop_lon = -83.0889; - feed.stops.put(s1.stop_id, s1); - - com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop(); - s2.stop_id = s2.stop_name = "s2"; - s2.stop_lat = 39.9621; - s2.stop_lon = -83.0007; - feed.stops.put(s2.stop_id, s2); - - com.conveyal.gtfs.model.Stop s3 = new com.conveyal.gtfs.model.Stop(); - s3.stop_id = s3.stop_name = "s3"; - s3.stop_lat = 39.9510; - s3.stop_lon = -83.0007; - feed.stops.put(s3.stop_id, s3); - - // make timetabled trips - for (int departure = 7 * 3600, dcount = 0; departure < 20 * 3600; departure += FREQUENCY) { - Trip t = new Trip(); - t.trip_id = "trip" + departure; - t.service_id = s.service_id; - t.route_id = r.route_id; - feed.trips.put(t.trip_id, t); - - StopTime st1 = new StopTime(); - st1.trip_id = t.trip_id; - st1.arrival_time = departure; - st1.departure_time = departure; - st1.stop_id = s1.stop_id; - st1.stop_sequence = 1; - feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1); - - // occasionally skip second stop - boolean secondStop = dcount++ % 10 != 0; - if (secondStop) { - StopTime st2 = new StopTime(); - st2.trip_id = t.trip_id; - st2.arrival_time = departure + TRAVEL_TIME; - st2.departure_time = departure + TRAVEL_TIME; - st2.stop_sequence = 2; - st2.stop_id = s2.stop_id; - feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2); - } - - StopTime st3 = new StopTime(); - st3.trip_id = t.trip_id; - st3.arrival_time = departure + (secondStop ? 2 : 1) * TRAVEL_TIME; - st3.departure_time = departure + (secondStop ? 2 : 1) * TRAVEL_TIME; - st3.stop_sequence = 3; - st3.stop_id = s3.stop_id; - feed.stop_times.put(new Fun.Tuple2(st3.trip_id, st3.stop_sequence), st3); - } - - File tempFile = File.createTempFile("gtfs", ".zip"); - feed.toFile(tempFile.getAbsolutePath()); - - // phew. load it into the graph. - GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile))); - gtfs.buildGraph(gg, new GraphBuilderModuleSummary(gtfs)); - } - /** Add a regular grid of stops to the graph */ public static void addRegularStopGrid(Graph g) { int count = 0; @@ -463,96 +130,6 @@ public static void addExtraStops (Graph g) { } } - /** Add transit (in both directions) to a Columbus graph */ - public static void addTransitBidirectional (Graph gg) throws Exception { - // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later - // once we're using the conveyal GTFS lib for everything we ought to be able to do this - // without even writing out the GTFS to a file. - GTFSFeed feed = new GTFSFeed(); - Agency a = createDummyAgency("agency", "Agency", "America/New_York"); - feed.agency.put("agency", a); - - Route r = new Route(); - r.route_short_name = "1"; - r.route_long_name = "High Street"; - r.route_type = 3; - r.agency_id = a.agency_id; - r.route_id = "route"; - feed.routes.put(r.route_id, r); - - Service s = createDummyService(); - feed.services.put(s.service_id, s); - - com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop(); - s1.stop_id = s1.stop_name = "s1"; - s1.stop_lat = 40.2182; - s1.stop_lon = -83.0889; - feed.stops.put(s1.stop_id, s1); - - com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop(); - s2.stop_id = s2.stop_name = "s2"; - s2.stop_lat = 39.9621; - s2.stop_lon = -83.0007; - feed.stops.put(s2.stop_id, s2); - - // make timetabled trips - for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) { - Trip t = new Trip(); - t.trip_id = "trip" + departure; - t.service_id = s.service_id; - t.route_id = r.route_id; - t.direction_id = 0; - feed.trips.put(t.trip_id, t); - - StopTime st1 = new StopTime(); - st1.trip_id = t.trip_id; - st1.arrival_time = departure; - st1.departure_time = departure; - st1.stop_id = s1.stop_id; - st1.stop_sequence = 1; - feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1); - - StopTime st2 = new StopTime(); - st2.trip_id = t.trip_id; - st2.arrival_time = departure + TRAVEL_TIME; - st2.departure_time = departure + TRAVEL_TIME; - st2.stop_sequence = 2; - st2.stop_id = s2.stop_id; - feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2); - - // opposite direction - t = new Trip(); - t.trip_id = "trip_back" + departure; - t.service_id = s.service_id; - t.route_id = r.route_id; - t.direction_id = 1; - feed.trips.put(t.trip_id, t); - - st1 = new StopTime(); - st1.trip_id = t.trip_id; - st1.arrival_time = departure; - st1.departure_time = departure; - st1.stop_id = s2.stop_id; - st1.stop_sequence = 1; - feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1); - - st2 = new StopTime(); - st2.trip_id = t.trip_id; - st2.arrival_time = departure + TRAVEL_TIME; - st2.departure_time = departure + TRAVEL_TIME; - st2.stop_sequence = 2; - st2.stop_id = s1.stop_id; - feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2); - } - - File tempFile = File.createTempFile("gtfs", ".zip"); - feed.toFile(tempFile.getAbsolutePath()); - - // phew. load it into the graph. - GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile))); - gtfs.buildGraph(gg, new GraphBuilderModuleSummary(gtfs)); - } - /** * Index the graph and then link all stations in the graph. */ @@ -564,4 +141,4 @@ public static void indexGraphAndLinkStations (Graph g) { linker.linkAllStationsToGraph(); } -} +} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java b/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java index f1591ba7274..604911b7fa8 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java @@ -3,9 +3,9 @@ import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import gnu.trove.iterator.TObjectIntIterator; import gnu.trove.map.TObjectIntMap; import gnu.trove.map.hash.TObjectIntHashMap; diff --git a/src/test/java/org/opentripplanner/graph_builder/module/map/TestStreetMatcher.java b/src/test/java/org/opentripplanner/graph_builder/module/map/TestStreetMatcher.java new file mode 100644 index 00000000000..462c1edcc4a --- /dev/null +++ b/src/test/java/org/opentripplanner/graph_builder/module/map/TestStreetMatcher.java @@ -0,0 +1,258 @@ +package org.opentripplanner.graph_builder.module.map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.opentripplanner.common.geometry.PackedCoordinateSequence; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.core.StateEditor; +import org.opentripplanner.routing.core.TraverseMode; +import org.opentripplanner.routing.core.TraverseModeSet; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.edgetype.StreetTraversalPermission; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.common.geometry.SphericalDistanceLibrary; +import org.opentripplanner.routing.vertextype.StreetVertex; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import java.util.Locale; +import org.opentripplanner.util.I18NString; +import org.opentripplanner.util.NonLocalizedString; + +public class TestStreetMatcher { + static GeometryFactory gf = new GeometryFactory(); + + private Graph graph; + + @Before + public void before() { + + graph = new Graph(); + + vertex("56th_24th", 47.669457, -122.387577); + vertex("56th_22nd", 47.669462, -122.384739); + vertex("56th_20th", 47.669457, -122.382106); + + vertex("market_24th", 47.668690, -122.387577); + vertex("market_ballard", 47.668683, -122.386096); + vertex("market_22nd", 47.668686, -122.384749); + vertex("market_leary", 47.668669, -122.384392); + vertex("market_russell", 47.668655, -122.382997); + vertex("market_20th", 47.668684, -122.382117); + + vertex("shilshole_24th", 47.668419, -122.387534); + vertex("shilshole_22nd", 47.666519, -122.384744); + vertex("shilshole_vernon", 47.665938, -122.384048); + vertex("shilshole_20th", 47.664356, -122.382192); + + vertex("ballard_turn", 47.668509, -122.386069); + vertex("ballard_22nd", 47.667624, -122.384744); + vertex("ballard_vernon", 47.666422, -122.383158); + vertex("ballard_20th", 47.665476, -122.382128); + + vertex("leary_vernon", 47.666863, -122.382353); + vertex("leary_20th", 47.666682, -122.382160); + + vertex("russell_20th", 47.667846, -122.382128); + + edges("56th_24th", "56th_22nd", "56th_20th"); + + edges("56th_24th", "market_24th"); + edges("56th_22nd", "market_22nd"); + edges("56th_20th", "market_20th"); + + edges("market_24th", "market_ballard", "market_22nd", "market_leary", "market_russell", + "market_20th"); + edges("market_24th", "shilshole_24th", "shilshole_22nd", "shilshole_vernon", + "shilshole_20th"); + edges("market_ballard", "ballard_turn", "ballard_22nd", "ballard_vernon", "ballard_20th"); + edges("market_leary", "leary_vernon", "leary_20th"); + edges("market_russell", "russell_20th"); + + edges("market_22nd", "ballard_22nd", "shilshole_22nd"); + edges("leary_vernon", "ballard_vernon", "shilshole_vernon"); + edges("market_20th", "russell_20th", "leary_20th", "ballard_20th", "shilshole_20th"); + + } + + @Test + public void testStreetMatcher() { + + LineString geometry = geometry(-122.385689, 47.669484, -122.387384, 47.669470); + + StreetMatcher matcher = new StreetMatcher(graph); + + List match = matcher.match(geometry); + assertNotNull(match); + assertEquals(1, match.size()); + assertEquals("56th_24th", match.get(0).getToVertex().getLabel()); + + geometry = geometry(-122.385689, 47.669484, -122.387384, 47.669470, -122.387588, 47.669325); + + match = matcher.match(geometry); + assertNotNull(match); + assertEquals(2, match.size()); + + geometry = geometry(-122.385689, 47.669484, -122.387384, 47.669470, -122.387588, 47.669325, + -122.387255, 47.668675); + + match = matcher.match(geometry); + assertNotNull(match); + assertEquals(3, match.size()); + + geometry = geometry(-122.384756, 47.669260, -122.384777, 47.667454, -122.383554, 47.666789, + -122.3825, 47.666); + match = matcher.match(geometry); + assertNotNull(match); + System.out.println(match); + assertEquals(4, match.size()); + assertEquals("ballard_20th", match.get(3).getToVertex().getLabel()); + } + + private LineString geometry(double... ordinates) { + Coordinate[] coords = new Coordinate[ordinates.length / 2]; + + for (int i = 0; i < ordinates.length; i += 2) { + coords[i / 2] = new Coordinate(ordinates[i], ordinates[i + 1]); + } + return gf.createLineString(coords); + } + + /**** + * Private Methods + ****/ + + private SimpleVertex vertex(String label, double lat, double lon) { + SimpleVertex v = new SimpleVertex(graph, label, lat, lon); + return v; + } + + private void edges(String... vLabels) { + for (int i = 0; i < vLabels.length - 1; i++) { + StreetVertex vA = (StreetVertex) graph.getVertex(vLabels[i]); + StreetVertex vB = (StreetVertex) graph.getVertex(vLabels[i + 1]); + + new SimpleEdge(vA, vB); + new SimpleEdge(vB, vA); + } + } + + private static class SimpleVertex extends StreetVertex { + + private static final long serialVersionUID = 1L; + + public SimpleVertex(Graph g, String label, double lat, double lon) { + super(g, label, lon, lat, new NonLocalizedString(label)); + } + } + + /* TODO explain why this exists and is "simple" */ + private static class SimpleEdge extends StreetEdge { + private static final long serialVersionUID = 1L; + + public SimpleEdge(StreetVertex v1, StreetVertex v2) { + super(v1, v2, null, (NonLocalizedString)null, 0, null, false); + } + + @Override + public State traverse(State s0) { + double d = getDistance(); + TraverseMode mode = s0.getNonTransitMode(); + int t = (int) (d / s0.getOptions().getSpeed(mode)); + StateEditor s1 = s0.edit(this); + s1.incrementTimeInSeconds(t); + s1.incrementWeight(d); + return s1.makeState(); + } + + @Override + public String getName() { + return null; + } + + @Override + public I18NString getRawName() { + return null; + } + + @Override + public String getName(Locale locale) { + return null; + } + + @Override + public LineString getGeometry() { + return gf.createLineString(new Coordinate[] { fromv.getCoordinate(), + tov.getCoordinate() }); + } + + @Override + public double getDistance() { + return SphericalDistanceLibrary.distance(getFromVertex().getCoordinate(), getToVertex().getCoordinate()); + } + + @Override + public PackedCoordinateSequence getElevationProfile() { + return null; + } + + @Override + public boolean canTraverse(TraverseModeSet modes) { + return true; + } + + @Override + public StreetTraversalPermission getPermission() { + return StreetTraversalPermission.ALL; + } + + @Override + public boolean isNoThruTraffic() { + return false; + } + + public String toString() { + return "SimpleEdge(" + fromv + ", " + tov + ")"; + } + + @Override + public int getStreetClass() { + return StreetEdge.CLASS_STREET; + } + + @Override + public boolean isWheelchairAccessible() { + return true; + } + + public boolean isElevationFlattened() { + return false; + } + + @Override + public float getCarSpeed() { + return 11.2f; + } + + @Override + public int getInAngle() { + return 0; + } + + @Override + public int getOutAngle() { + return 0; + } + + @Override + public void setCarSpeed(float carSpeed) {} + + } +} diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/TestIntermediatePlaces.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/TestIntermediatePlaces.java index 1e5f931a14e..bc9597afc25 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/osm/TestIntermediatePlaces.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/osm/TestIntermediatePlaces.java @@ -36,7 +36,7 @@ public class TestIntermediatePlaces { /** - * The spacial deviation that we allow in degrees + * The spatial deviation that we allow in degrees */ public static final double DELTA = 0.005; @@ -56,12 +56,9 @@ public class TestIntermediatePlaces { Router router = otpServer.getGraphService().getRouter("A"); TestIntermediatePlaces.graphPathFinder = new GraphPathFinder(router); timeZone = graph.getTimeZone(); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - assert false : "Could not build graph: " + e.getMessage(); } catch (Exception e) { e.printStackTrace(); - assert false : "Could not add transit data: " + e.getMessage(); + assert false : "Could not add transit data: " + e.toString(); } } diff --git a/src/test/java/org/opentripplanner/graph_builder/module/shapefile/TestShapefileStreetGraphBuilderImpl.java b/src/test/java/org/opentripplanner/graph_builder/module/shapefile/TestShapefileStreetGraphBuilderImpl.java index 02f2a6d142c..442a2b26ed2 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/shapefile/TestShapefileStreetGraphBuilderImpl.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/shapefile/TestShapefileStreetGraphBuilderImpl.java @@ -18,7 +18,7 @@ import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.spt.ShortestPathTree; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; public class TestShapefileStreetGraphBuilderImpl extends TestCase { diff --git a/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceImplTest.java b/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceImplTest.java index 036725ed162..a50284a3134 100644 --- a/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceImplTest.java +++ b/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceImplTest.java @@ -24,7 +24,9 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.List; import static java.util.Comparator.comparing; @@ -279,9 +281,10 @@ private static ServiceCalendarDate createAServiceCalendarDateExclution(FeedScope return date; } - private static T first(Collection c) { - //noinspection ConstantConditions - return c.stream().sorted(comparing(T::toString)).findFirst().get(); + private static T first(Collection collection) { + List items = new ArrayList<>(collection); + items.sort(comparing(Object::toString)); + return items.isEmpty() ? null : items.get(0); } private static String toString(ShapePoint sp) { diff --git a/src/test/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializerTest.java b/src/test/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializerTest.java index eff72fe3252..7212d5d1a3a 100644 --- a/src/test/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/json_serialization/EncodedPolylineJSONSerializerTest.java @@ -1,9 +1,9 @@ package org.opentripplanner.model.json_serialization; import com.fasterxml.jackson.core.*; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; import junit.framework.TestCase; import org.junit.Test; import org.opentripplanner.common.geometry.GeometryUtils; @@ -459,5 +459,10 @@ public byte[] getBinaryValue(Base64Variant base64Variant) throws IOException { public String getValueAsString(String s) throws IOException { return stringProducer.get(); } + + @Override + public boolean hasToken(JsonToken t) { + return false; + } } -} \ No newline at end of file +} diff --git a/src/test/java/org/opentripplanner/profile/ConvertToFrequencyTest.java b/src/test/java/org/opentripplanner/profile/ConvertToFrequencyTest.java deleted file mode 100644 index 461a835012c..00000000000 --- a/src/test/java/org/opentripplanner/profile/ConvertToFrequencyTest.java +++ /dev/null @@ -1,215 +0,0 @@ -package org.opentripplanner.profile; - -import junit.framework.TestCase; -import org.joda.time.LocalDate; -import org.junit.Test; -import org.opentripplanner.analyst.scenario.ConvertToFrequency; -import org.opentripplanner.analyst.scenario.Scenario; -import org.opentripplanner.api.parameter.QualifiedModeSet; -import org.opentripplanner.graph_builder.module.FakeGraph; -import org.opentripplanner.profile.ProfileRequest; -import org.opentripplanner.profile.RaptorWorkerTimetable; -import org.opentripplanner.profile.RepeatedRaptorProfileRouter; -import org.opentripplanner.routing.core.TraverseModeSet; -import org.opentripplanner.routing.graph.Graph; - -import java.util.Arrays; - -import static org.opentripplanner.graph_builder.module.FakeGraph.*; - -/** - * Make sure that converting lines to frequency-based representations works. - */ -public class ConvertToFrequencyTest extends TestCase { - /** The simplest case of frequency conversion: no weird loop routes or anything like that, travel times always same, etc. */ - @Test - public void testSimpleConversion () throws Exception { - Graph gg = buildGraphNoTransit(); - addTransit(gg); - indexGraphAndLinkStations(gg); - - ProfileRequest pr1 = new ProfileRequest(); - pr1.date = new LocalDate(2015, 6, 10); - pr1.fromTime = 7 * 3600; - pr1.toTime = 9 * 3600; - pr1.fromLat = pr1.toLat = 39.9621; - pr1.fromLon = pr1.toLon = -83.0007; - pr1.accessModes = pr1.egressModes = pr1.directModes = new QualifiedModeSet("WALK"); - pr1.transitModes = new TraverseModeSet("TRANSIT"); - - RepeatedRaptorProfileRouter rrpr1 = new RepeatedRaptorProfileRouter(gg, pr1); - rrpr1.route(); - - ProfileRequest pr2 = new ProfileRequest(); - pr2.date = new LocalDate(2015, 6, 10); - pr2.fromTime = 7 * 3600; - pr2.toTime = 9 * 3600; - pr2.fromLat = pr2.toLat = 39.9621; - pr2.fromLon = pr2.toLon = -83.0007; - pr2.accessModes = pr2.egressModes = pr2.directModes = new QualifiedModeSet("WALK"); - pr2.transitModes = new TraverseModeSet("TRANSIT"); - - ConvertToFrequency ctf = new ConvertToFrequency(); - ctf.groupBy = ConvertToFrequency.ConversionGroup.ROUTE_DIRECTION; - ctf.routeId = new String [] { "route" }; - ctf.windowStart = 5 * 3600; - ctf.windowEnd = 10 * 3600; - - pr2.scenario = new Scenario(0); - pr2.scenario.modifications = Arrays.asList(ctf); - - RepeatedRaptorProfileRouter rrpr2 = new RepeatedRaptorProfileRouter(gg, pr2); - rrpr2.route(); - - assertFalse(rrpr1.raptorWorkerData.hasFrequencies); - assertTrue(rrpr2.raptorWorkerData.hasFrequencies); - - RaptorWorkerTimetable tt = rrpr2.raptorWorkerData.timetablesForPattern.get(0); - assertEquals(FakeGraph.FREQUENCY, tt.headwaySecs[0]); - assertEquals(FakeGraph.TRAVEL_TIME, tt.frequencyTrips[0][2]); - } - - /** - * Test the case where there are multiple patterns that need to be chosen from. - */ - @Test - public void testMultiplePatterns () throws Exception { - Graph gg = buildGraphNoTransit(); - addMultiplePatterns(gg); - indexGraphAndLinkStations(gg); - - ProfileRequest pr1 = new ProfileRequest(); - pr1.date = new LocalDate(2015, 6, 10); - pr1.fromTime = 7 * 3600; - pr1.toTime = 9 * 3600; - pr1.fromLat = pr1.toLat = 39.9621; - pr1.fromLon = pr1.toLon = -83.0007; - pr1.accessModes = pr1.egressModes = pr1.directModes = new QualifiedModeSet("WALK"); - pr1.transitModes = new TraverseModeSet("TRANSIT"); - - RepeatedRaptorProfileRouter rrpr1 = new RepeatedRaptorProfileRouter(gg, pr1); - rrpr1.route(); - - ProfileRequest pr2 = new ProfileRequest(); - pr2.date = new LocalDate(2015, 6, 10); - pr2.fromTime = 7 * 3600; - pr2.toTime = 9 * 3600; - pr2.fromLat = pr2.toLat = 39.9621; - pr2.fromLon = pr2.toLon = -83.0007; - pr2.accessModes = pr2.egressModes = pr2.directModes = new QualifiedModeSet("WALK"); - pr2.transitModes = new TraverseModeSet("TRANSIT"); - - ConvertToFrequency ctf = new ConvertToFrequency(); - ctf.groupBy = ConvertToFrequency.ConversionGroup.ROUTE_DIRECTION; - ctf.routeId = new String [] { "route" }; - ctf.windowStart = 5 * 3600; - ctf.windowEnd = 10 * 3600; - - pr2.scenario = new Scenario(0); - pr2.scenario.modifications = Arrays.asList(ctf); - - RepeatedRaptorProfileRouter rrpr2 = new RepeatedRaptorProfileRouter(gg, pr2); - rrpr2.route(); - - assertFalse(rrpr1.raptorWorkerData.hasFrequencies); - assertTrue(rrpr2.raptorWorkerData.hasFrequencies); - - // everything should have gotten merged into one pattern - assertEquals(1, rrpr2.raptorWorkerData.timetablesForPattern.size()); - RaptorWorkerTimetable tt = rrpr2.raptorWorkerData.timetablesForPattern.get(0); - - // should be no frequency variation because trips on all patterns are considered for frequencies. - // there should be no travel time variation because only trips on the dominant pattern are considered - // for travel time. - assertEquals(FakeGraph.FREQUENCY, tt.headwaySecs[0]); - assertEquals(FakeGraph.TRAVEL_TIME, tt.frequencyTrips[0][2]); - - // now try it with groupings by pattern - ConvertToFrequency ctf3 = new ConvertToFrequency(); - ctf3.groupBy = ConvertToFrequency.ConversionGroup.PATTERN; - ctf3.routeId = new String [] { "route" }; - ctf3.windowStart = 5 * 3600; - ctf3.windowEnd = 10 * 3600; - - ProfileRequest pr3 = new ProfileRequest(); - pr3.date = new LocalDate(2015, 6, 10); - pr3.fromTime = 7 * 3600; - pr3.toTime = 9 * 3600; - pr3.fromLat = pr2.toLat = 39.9621; - pr3.fromLon = pr2.toLon = -83.0007; - pr3.accessModes = pr2.egressModes = pr2.directModes = new QualifiedModeSet("WALK"); - pr3.transitModes = new TraverseModeSet("TRANSIT"); - pr3.scenario = new Scenario(0); - pr3.scenario.modifications = Arrays.asList(ctf3); - - RepeatedRaptorProfileRouter rrpr3 = new RepeatedRaptorProfileRouter(gg, pr3); - rrpr3.route(); - - assertTrue(rrpr3.raptorWorkerData.hasFrequencies); - // should be converted to two independent patterns - assertEquals(2, rrpr3.raptorWorkerData.timetablesForPattern.size()); - - RaptorWorkerTimetable shrt, lng; - if (rrpr3.raptorWorkerData.timetablesForPattern.get(0).nStops == 2) { - shrt = rrpr3.raptorWorkerData.timetablesForPattern.get(0); - lng = rrpr3.raptorWorkerData.timetablesForPattern.get(1); - } else { - lng = rrpr3.raptorWorkerData.timetablesForPattern.get(0); - shrt = rrpr3.raptorWorkerData.timetablesForPattern.get(1); - } - - assertEquals(3, lng.nStops); - assertEquals(2, shrt.nStops); - - assertEquals(675, lng.headwaySecs[0]); - assertEquals((int) (FakeGraph.FREQUENCY / 0.1), shrt.headwaySecs[0]); - - // make sure that the hop time is always FakeGraph.TRAVEL_TIME - assertEquals(FakeGraph.TRAVEL_TIME, shrt.frequencyTrips[0][2]); - assertEquals(FakeGraph.TRAVEL_TIME, lng.frequencyTrips[0][2]); - assertEquals(FakeGraph.TRAVEL_TIME * 2, lng.frequencyTrips[0][4]); - } - - /** Test bidirectional conversion */ - @Test - public void testBidirectional () throws Exception { - Graph gg = buildGraphNoTransit(); - addTransitBidirectional(gg); - indexGraphAndLinkStations(gg); - - ProfileRequest pr2 = new ProfileRequest(); - pr2.date = new LocalDate(2015, 6, 10); - pr2.fromTime = 7 * 3600; - pr2.toTime = 9 * 3600; - pr2.fromLat = pr2.toLat = 39.9621; - pr2.fromLon = pr2.toLon = -83.0007; - pr2.accessModes = pr2.egressModes = pr2.directModes = new QualifiedModeSet("WALK"); - pr2.transitModes = new TraverseModeSet("TRANSIT"); - - ConvertToFrequency ctf = new ConvertToFrequency(); - ctf.groupBy = ConvertToFrequency.ConversionGroup.ROUTE_DIRECTION; - ctf.routeId = new String [] { "route" }; - ctf.windowStart = 5 * 3600; - ctf.windowEnd = 10 * 3600; - - pr2.scenario = new Scenario(0); - pr2.scenario.modifications = Arrays.asList(ctf); - - RepeatedRaptorProfileRouter rrpr2 = new RepeatedRaptorProfileRouter(gg, pr2); - rrpr2.route(); - - assertTrue(rrpr2.raptorWorkerData.hasFrequencies); - - assertEquals(2, rrpr2.raptorWorkerData.timetablesForPattern.size()); - - // make sure we got trips in both directions - RaptorWorkerTimetable tt = rrpr2.raptorWorkerData.timetablesForPattern.get(0); - RaptorWorkerTimetable tt2 = rrpr2.raptorWorkerData.timetablesForPattern.get(1); - - assertEquals(2, tt2.stopIndices.length); - assertEquals(2, tt.stopIndices.length); - - assertEquals(tt.stopIndices[0], tt2.stopIndices[1]); - assertEquals(tt.stopIndices[1], tt2.stopIndices[0]); - } -} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/routing/TestHalfEdges.java b/src/test/java/org/opentripplanner/routing/TestHalfEdges.java index 36e8e0068f3..3592167e1d0 100644 --- a/src/test/java/org/opentripplanner/routing/TestHalfEdges.java +++ b/src/test/java/org/opentripplanner/routing/TestHalfEdges.java @@ -1,9 +1,9 @@ package org.opentripplanner.routing; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.linearref.LinearLocation; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.linearref.LinearLocation; import org.junit.Before; import org.junit.Rule; import org.junit.Test; diff --git a/src/test/java/org/opentripplanner/routing/algorithm/AStarTest.java b/src/test/java/org/opentripplanner/routing/algorithm/AStarTest.java index f8356c580b3..5a3293580d4 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/AStarTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/AStarTest.java @@ -23,7 +23,7 @@ import org.opentripplanner.routing.spt.GraphPath; import org.opentripplanner.routing.spt.ShortestPathTree; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import org.opentripplanner.util.NonLocalizedString; public class AStarTest { @@ -190,7 +190,6 @@ public void testBackExtraEdges() { options.setRoutingContext(graph, from, to); ShortestPathTree tree = new AStar().getShortestPathTree(options); - options.cleanup(); GraphPath path = tree.getPath(from, false); @@ -207,6 +206,8 @@ public void testBackExtraEdges() { assertEquals("market_20th", states.get(6).getVertex().getLabel()); assertEquals("56th_20th", states.get(7).getVertex().getLabel()); assertEquals("near_56th_20th", states.get(8).getVertex().getLabel()); + + options.cleanup(); } @Test diff --git a/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java b/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java index 24e2e8856e7..37efd94ea04 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java @@ -23,8 +23,8 @@ import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; public class TurnCostTest { diff --git a/src/test/java/org/opentripplanner/routing/algorithm/TurnRestrictionTest.java b/src/test/java/org/opentripplanner/routing/algorithm/TurnRestrictionTest.java index 5cdf807efe4..f7517ec79a5 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/TurnRestrictionTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/TurnRestrictionTest.java @@ -22,8 +22,8 @@ import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; public class TurnRestrictionTest { diff --git a/src/test/java/org/opentripplanner/routing/core/RoutingContextDestroyTest.java b/src/test/java/org/opentripplanner/routing/core/RoutingContextDestroyTest.java index ea31c8c752b..c7e8e189fcf 100644 --- a/src/test/java/org/opentripplanner/routing/core/RoutingContextDestroyTest.java +++ b/src/test/java/org/opentripplanner/routing/core/RoutingContextDestroyTest.java @@ -1,8 +1,8 @@ package org.opentripplanner.routing.core; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.junit.Before; import org.junit.Test; import org.opentripplanner.common.geometry.GeometryUtils; diff --git a/src/test/java/org/opentripplanner/routing/core/RoutingContextTest.java b/src/test/java/org/opentripplanner/routing/core/RoutingContextTest.java index 5fc87c32bf0..5721562ca7a 100644 --- a/src/test/java/org/opentripplanner/routing/core/RoutingContextTest.java +++ b/src/test/java/org/opentripplanner/routing/core/RoutingContextTest.java @@ -34,6 +34,8 @@ public void testSetServiceDays() throws Exception { routingRequest.modes = new TraverseModeSet("WALK,TRANSIT"); when(graph.getTimeZone()).thenReturn(TimeZone.getTimeZone("Europe/Budapest")); + when(graph.getAllTimeZones()).thenReturn(Collections.singletonList( + TimeZone.getTimeZone("Europe/Budapest"))); when(graph.getCalendarService()).thenReturn(calendarService); when(graph.getFeedIds()).thenReturn(Collections.singletonList("FEED")); when(graph.getAgencies(feedId)).thenReturn(Collections.singletonList(agency)); diff --git a/src/test/java/org/opentripplanner/routing/core/ServiceDayLookoutTest.java b/src/test/java/org/opentripplanner/routing/core/ServiceDayLookoutTest.java new file mode 100644 index 00000000000..922fbed2538 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/core/ServiceDayLookoutTest.java @@ -0,0 +1,66 @@ +/* This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ +package org.opentripplanner.routing.core; + +import org.junit.Test; +import org.opentripplanner.GtfsTest; +import org.opentripplanner.api.model.Leg; +import org.opentripplanner.util.TestUtils; + +public class ServiceDayLookoutTest extends GtfsTest { + + @Override + public String getFeedName() { + return "vermont/ruralcommunity-flex-vt-us.zip"; + } + + // Kingdom Shopper 2 runs 2nd and 4th Wednesday of every month + + @Test + public void testLookoutBaseline() { + long time = TestUtils.dateInSeconds("America/New_York", 2018, 5, 25, 8, 0, 0); // monday + Leg leg = plan(time, "804108", "804106", null, false, false, + null, null, null); + assertNull(leg); + } + + @Test + public void testLookout() { + long time = TestUtils.dateInSeconds("America/New_York", 2018, 5, 25, 8, 0, 0); // monday + RoutingRequest opt = new RoutingRequest(); + opt.serviceDayLookout = 2; + Leg leg = plan(time, "804108", "804106", null, false, false, + null, null, null, 1, opt)[0]; + assertNotNull(leg); + assertEquals("3116", leg.routeId.getId()); + } + + @Test + public void testLookoutBaselineArriveBy() { + long time = TestUtils.dateInSeconds("America/New_York", 2018, 5, 25, 8, 0, 0); // monday + Leg leg = plan(-1 * time, "804108", "804106", null, false, false, + null, null, null); + assertNull(leg); + } + + @Test + public void testLookoutArriveBy() { + long time = TestUtils.dateInSeconds("America/New_York", 2018, 5, 25, 8, 0, 0); // monday + RoutingRequest opt = new RoutingRequest(); + opt.serviceDayLookout = 14; // look back two weeks + Leg leg = plan(-1 * time, "804108", "804106", null, false, false, + null, null, null, 1, opt)[0]; + assertNotNull(leg); + assertEquals("3116", leg.routeId.getId()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/core/SimpleTraversalCostModelTest.java b/src/test/java/org/opentripplanner/routing/core/SimpleTraversalCostModelTest.java index 83dd4920f31..188c98581f1 100644 --- a/src/test/java/org/opentripplanner/routing/core/SimpleTraversalCostModelTest.java +++ b/src/test/java/org/opentripplanner/routing/core/SimpleTraversalCostModelTest.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.core; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.junit.Before; import org.junit.Test; import org.opentripplanner.common.geometry.GeometryUtils; diff --git a/src/test/java/org/opentripplanner/routing/core/TestGraph.java b/src/test/java/org/opentripplanner/routing/core/TestGraph.java index ebeaa75db00..53b3dceae80 100644 --- a/src/test/java/org/opentripplanner/routing/core/TestGraph.java +++ b/src/test/java/org/opentripplanner/routing/core/TestGraph.java @@ -17,8 +17,8 @@ import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; public class TestGraph extends TestCase { public void testBasic() throws Exception { diff --git a/src/test/java/org/opentripplanner/routing/core/TestTurns.java b/src/test/java/org/opentripplanner/routing/core/TestTurns.java index 57c4e6561b0..da2c190cc88 100644 --- a/src/test/java/org/opentripplanner/routing/core/TestTurns.java +++ b/src/test/java/org/opentripplanner/routing/core/TestTurns.java @@ -4,9 +4,9 @@ import org.opentripplanner.routing.edgetype.StreetTraversalPermission; import org.opentripplanner.routing.vertextype.IntersectionVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.GeometryFactory; import junit.framework.TestCase; diff --git a/src/test/java/org/opentripplanner/routing/edgetype/PlainStreetEdgeTest.java b/src/test/java/org/opentripplanner/routing/edgetype/PlainStreetEdgeTest.java index b145cc94f97..c7343cd8277 100644 --- a/src/test/java/org/opentripplanner/routing/edgetype/PlainStreetEdgeTest.java +++ b/src/test/java/org/opentripplanner/routing/edgetype/PlainStreetEdgeTest.java @@ -14,8 +14,8 @@ import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; public class PlainStreetEdgeTest { diff --git a/src/test/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdgeTest.java b/src/test/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdgeTest.java index 7dd4ffa46a1..023d5860db3 100644 --- a/src/test/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdgeTest.java +++ b/src/test/java/org/opentripplanner/routing/edgetype/TemporaryPartialStreetEdgeTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.Before; @@ -21,8 +22,8 @@ import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.util.NonLocalizedString; public class TemporaryPartialStreetEdgeTest { @@ -109,6 +110,7 @@ public void testTraversalOfSubdividedEdge() { RoutingRequest options = new RoutingRequest(); options.setMode(TraverseMode.CAR); options.setRoutingContext(graph, v1, v2); + options.rctx.temporaryVertices.addAll(Arrays.asList(end, start)); // All intersections take 10 minutes - we'll notice if one isn't counted. double turnDurationSecs = 10.0 * 60.0; diff --git a/src/test/java/org/opentripplanner/routing/edgetype/TestTriangle.java b/src/test/java/org/opentripplanner/routing/edgetype/TestTriangle.java index 15ac525b402..39064b58648 100644 --- a/src/test/java/org/opentripplanner/routing/edgetype/TestTriangle.java +++ b/src/test/java/org/opentripplanner/routing/edgetype/TestTriangle.java @@ -12,9 +12,9 @@ import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.util.NonLocalizedString; public class TestTriangle extends TestCase { diff --git a/src/test/java/org/opentripplanner/routing/edgetype/loader/TestPatternHopFactory.java b/src/test/java/org/opentripplanner/routing/edgetype/loader/TestPatternHopFactory.java index 646b9585a47..cd511a711cf 100644 --- a/src/test/java/org/opentripplanner/routing/edgetype/loader/TestPatternHopFactory.java +++ b/src/test/java/org/opentripplanner/routing/edgetype/loader/TestPatternHopFactory.java @@ -46,7 +46,7 @@ import org.opentripplanner.routing.vertextype.TransitStopDepart; import org.opentripplanner.util.TestUtils; -import com.vividsolutions.jts.geom.Geometry; +import org.locationtech.jts.geom.Geometry; import static org.opentripplanner.calendar.impl.CalendarServiceDataFactoryImpl.createCalendarServiceData; diff --git a/src/test/java/org/opentripplanner/routing/flex/Ride.java b/src/test/java/org/opentripplanner/routing/flex/Ride.java new file mode 100644 index 00000000000..3a54753ee0f --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/flex/Ride.java @@ -0,0 +1,148 @@ +package org.opentripplanner.routing.flex; + +import org.opentripplanner.api.model.BoardAlightType; +import org.opentripplanner.model.FeedScopedId; +import org.opentripplanner.model.Stop; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.edgetype.HopEdge; +import org.opentripplanner.routing.edgetype.flex.PartialPatternHop; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.spt.GraphPath; + +import java.util.ArrayList; +import java.util.List; + +/** + * Model a transit trip so that assertions can be made against it + * + * Similar to DefaultFareServiceImpl.Ride - could be merged + * Cherry-picked from MTA code + * */ +public class Ride { + + private String startZone; + + private String endZone; + + private List zones = new ArrayList<>(); + + private FeedScopedId route; + + private String agency; + + private long startTime; + + private long endTime; + + private Stop firstStop; + + private Stop lastStop; + + private FeedScopedId trip; + + private BoardAlightType boardType = BoardAlightType.DEFAULT; + + private BoardAlightType alightType = BoardAlightType.DEFAULT; + + public String getStartZone() { + return startZone; + } + + public String getEndZone() { + return endZone; + } + + public List getZones() { + return zones; + } + + public FeedScopedId getRoute() { + return route; + } + + public String getAgency() { + return agency; + } + + public long getStartTime() { + return startTime; + } + + public long getEndTime() { + return endTime; + } + + public Stop getFirstStop() { + return firstStop; + } + + public Stop getLastStop() { + return lastStop; + } + + public FeedScopedId getTrip() { + return trip; + } + + public String getFirstStopId() { + return firstStop.getId().getId(); + } + + public String getLastStopId() { + return lastStop.getId().getId(); + } + + public BoardAlightType getBoardType() { + return boardType; + } + + public BoardAlightType getAlightType() { + return alightType; + } + + public static List createRides(GraphPath path) { + List rides = new ArrayList<>(); + Ride ride = null; + for (State state : path.states) { + Edge edge = state.getBackEdge(); + if ( ! (edge instanceof HopEdge)) + continue; + HopEdge hEdge = (HopEdge) edge; + if (ride == null || ! state.getRoute().equals(ride.route)) { + ride = new Ride(); + rides.add(ride); + ride.startZone = hEdge.getBeginStop().getZoneId(); + ride.zones.add(ride.startZone); + ride.agency = state.getBackTrip().getRoute().getAgency().getId(); + ride.route = state.getRoute(); + ride.startTime = state.getBackState().getTimeSeconds(); + ride.firstStop = hEdge.getBeginStop(); + ride.trip = state.getTripId(); + if (hEdge instanceof PartialPatternHop) { + PartialPatternHop hop = (PartialPatternHop) hEdge; + if (hop.isFlagStopBoard()) { + ride.boardType = BoardAlightType.FLAG_STOP; + } else if (hop.isDeviatedRouteBoard()) { + ride.boardType = BoardAlightType.DEVIATED; + } + } + } + ride.lastStop = hEdge.getEndStop(); + ride.endZone = ride.lastStop.getZoneId(); + ride.zones.add(ride.endZone); + ride.endTime = state.getTimeSeconds(); + ride.alightType = BoardAlightType.DEFAULT; + if (hEdge instanceof PartialPatternHop) { + PartialPatternHop hop = (PartialPatternHop) hEdge; + if (hop.isFlagStopAlight()) { + ride.alightType = BoardAlightType.FLAG_STOP; + } else if (hop.isDeviatedRouteAlight()) { + ride.alightType = BoardAlightType.DEVIATED; + } + } + + // in default fare service, classify rides by mode + } + return rides; + } +} diff --git a/src/test/java/org/opentripplanner/routing/flex/VermontFlexRoutingTest.java b/src/test/java/org/opentripplanner/routing/flex/VermontFlexRoutingTest.java new file mode 100644 index 00000000000..9efad8720cb --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/flex/VermontFlexRoutingTest.java @@ -0,0 +1,262 @@ +package org.opentripplanner.routing.flex; + +import org.junit.Test; +import org.opentripplanner.ConstantsForTests; +import org.opentripplanner.api.model.BoardAlightType; +import org.opentripplanner.routing.algorithm.AStar; +import org.opentripplanner.routing.core.Fare; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.impl.GraphPathFinder; +import org.opentripplanner.routing.services.FareService; +import org.opentripplanner.routing.spt.GraphPath; +import org.opentripplanner.standalone.Router; +import org.opentripplanner.util.DateUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.*; + +public class VermontFlexRoutingTest { + + private static final Logger LOG = LoggerFactory.getLogger(VermontFlexRoutingTest.class); + + private Graph graph = ConstantsForTests.getInstance().getVermontGraph(); + private static final int MAX_WALK_DISTANCE = 804; + private static final double CALL_AND_RIDE_RELUCTANCE = 3.0; + private static final double WALK_RELUCTANCE = 3.0; + private static final double WAIT_AT_BEGINNING_FACTOR = 0; + private static final int TRANSFER_PENALTY = 600; + private static final boolean IGNORE_DRT_ADVANCE_MIN_BOOKING = true; + + // Flag stop on both sides, on Jay-Lyn (route 1382) + @Test + public void testFlagStop() { + RoutingRequest options = buildRequest("44.4214596,-72.019371", "44.4277732,-72.01203514", + "2018-05-23", "1:37pm"); + GraphPath path = getPathToDestination(options); + List rides = Ride.createRides(path); + assertEquals(1, rides.size()); + Ride ride = rides.get(0); + assertEquals("1382", ride.getRoute().getId()); + assertEquals(BoardAlightType.FLAG_STOP, ride.getBoardType()); + assertEquals(BoardAlightType.FLAG_STOP, ride.getAlightType()); + checkFare(path); + options.rctx.destroy(); + } + + // Deviated Route on both ends + @Test + public void testCallAndRide() { + RoutingRequest options = buildRequest("44.38485134435363,-72.05881118774415", "44.422379116722084,-72.0198440551758", + "2018-05-23", "1:37pm"); + GraphPath path = getPathToDestination(options); + List rides = Ride.createRides(path); + assertEquals(1, rides.size()); + Ride ride = rides.get(0); + assertEquals("7415", ride.getRoute().getId()); + assertEquals(BoardAlightType.DEVIATED, ride.getBoardType()); + assertEquals(BoardAlightType.DEVIATED, ride.getBoardType()); + checkFare(path); + + // Check that times are respected. DAR is available 1pm-3pm + + // arriveBy=false Check start time respected. If request is for before 1pm, we should get a trip starting at 1pm. + path = getPathToDestination( + buildRequest("44.38485134435363,-72.05881118774415", "44.422379116722084,-72.0198440551758", + "2018-05-23", "12:50pm") + ); + rides = Ride.createRides(path); + assertEquals(1, rides.size()); + ride = rides.get(0); + assertEquals("7415", ride.getRoute().getId()); + assertDateEquals(ride.getStartTime(), "2018-05-23", "1:00pm", graph.getTimeZone()); + + // arriveBy=false Check end time respected. + path = getPathToDestination( + buildRequest("44.38485134435363,-72.05881118774415", "44.422379116722084,-72.0198440551758", + "2018-05-23", "2:56pm") + ); + rides = Ride.createRides(path); + assertEquals(1, rides.size()); + ride = rides.get(0); + assertEquals("7415", ride.getRoute().getId()); + assertDateEquals(ride.getStartTime(), "2018-05-24", "1:00pm", graph.getTimeZone()); + + // arriveBy=true Check start time respected. + path = getPathToDestination( + buildRequest("44.38485134435363,-72.05881118774415", "44.422379116722084,-72.0198440551758", + "2018-05-24", "1:05pm", true) + ); + rides = Ride.createRides(path); + assertEquals(1, rides.size()); + ride = rides.get(0); + assertEquals("7415", ride.getRoute().getId()); + assertDateEquals(ride.getEndTime(), "2018-05-23", "3:00pm", graph.getTimeZone()); + + // arriveBy=true Check end time respected + path = getPathToDestination( + buildRequest("44.38485134435363,-72.05881118774415", "44.422379116722084,-72.0198440551758", + "2018-05-24", "3:05pm", true) + ); + rides = Ride.createRides(path); + assertEquals(1, rides.size()); + ride = rides.get(0); + assertEquals("7415", ride.getRoute().getId()); + assertDateEquals(ride.getEndTime(), "2018-05-24", "3:00pm", graph.getTimeZone()); + options.rctx.destroy(); + } + + // Deviated Fixed Route at both ends + @Test + public void testDeviatedFixedRoute() { + RoutingRequest options = buildRequest("44.950950106914135,-72.20008850097658", "44.94985671536269,-72.13708877563478", + "2018-05-23", "4:00pm"); + GraphPath path = getPathToDestination(options); + List rides = Ride.createRides(path); + assertEquals(1, rides.size()); + Ride ride = rides.get(0); + assertEquals("1383", ride.getRoute().getId()); + assertEquals(BoardAlightType.DEVIATED, ride.getBoardType()); + assertEquals(BoardAlightType.DEVIATED, ride.getAlightType()); + checkFare(path); + options.rctx.destroy(); + } + + // Flag stop to a deviated fixed route that starts as a regular route and ends deviated + @Test + public void testFlagStopToRegularStopEndingInDeviatedFixedRoute() { + RoutingRequest options = buildRequest("44.8091683,-72.20580269999999", "44.94985671536269,-72.13708877563478", + "2018-06-13", "9:30am"); + GraphPath path = getPathToDestination(options); + List rides = Ride.createRides(path); + assertEquals(2, rides.size()); + Ride ride = rides.get(0); + assertEquals("3116", ride.getRoute().getId()); + assertEquals(BoardAlightType.FLAG_STOP, ride.getBoardType()); + assertEquals(BoardAlightType.DEFAULT, ride.getAlightType()); + + Ride ride2 = rides.get(1); + assertEquals("1383", ride2.getRoute().getId()); + assertEquals(BoardAlightType.DEFAULT, ride2.getBoardType()); + assertEquals(BoardAlightType.DEVIATED, ride2.getAlightType()); + checkFare(path); + options.rctx.destroy(); + } + + // Deviated Route on both ends + @Test + public void testMultipleThreads() throws InterruptedException, ExecutionException { + AtomicBoolean thread1SetupComplete = new AtomicBoolean(false), + thread2SearchComplete = new AtomicBoolean(false); + + FutureTask task1 = new FutureTask<>(() -> { + boolean success = true; + LOG.info("Thread 1 starting"); + RoutingRequest options = buildRequest("44.4214596,-72.019371", "44.4277732,-72.01203514", + "2018-05-23", "1:37pm"); + GraphPath path = getPathToDestination(options); + thread1SetupComplete.set(true); + List rides = Ride.createRides(path); + assertEquals(1, rides.size()); + Ride ride = rides.get(0); + success &= "1382".equals(ride.getRoute().getId()); + success &= BoardAlightType.FLAG_STOP.equals(ride.getBoardType()); + success &= BoardAlightType.FLAG_STOP.equals(ride.getAlightType()); + while (!thread2SearchComplete.get()) ; + LOG.info("Thread 1 cleaning up..."); + options.rctx.destroy(); + LOG.info("Thread 1 complete."); + return success; + }); + + FutureTask task2 = new FutureTask<>(() -> { + boolean success = true; + while(!thread1SetupComplete.get()); + LOG.info("Thread 2 starting"); + RoutingRequest options = buildRequest("44.4214596,-72.019371", "44.4277732,-72.01203514", + "2018-05-23", "1:37pm"); + // Don't make temporary edges + AStar astar = new AStar(); + astar.getShortestPathTree(options); + GraphPath path = astar.getPathsToTarget().iterator().next(); + List rides = Ride.createRides(path); + // possibly no rides if just walking. + if (!rides.isEmpty()) { + Ride ride = rides.get(0); + success &= !BoardAlightType.FLAG_STOP.equals(ride.getBoardType()); + success &= !BoardAlightType.FLAG_STOP.equals(ride.getAlightType()); + } + LOG.info("Thread 2 cleaning up..."); + options.rctx.destroy(); + LOG.info("Thread 2 complete"); + thread2SearchComplete.set(true); + return success; + }); + + new Thread(task1).start(); + new Thread(task2).start(); + + assertTrue(task1.get()); + assertTrue(task2.get()); + } + + private RoutingRequest buildRequest(String from, String to, String date, String time) { + return buildRequest(from, to, date, time, false); + } + + private RoutingRequest buildRequest(String from, String to, String date, String time, boolean arriveBy) { + return buildRequest(from, to, date, time, arriveBy, MAX_WALK_DISTANCE, + CALL_AND_RIDE_RELUCTANCE, WALK_RELUCTANCE, WAIT_AT_BEGINNING_FACTOR, + TRANSFER_PENALTY, IGNORE_DRT_ADVANCE_MIN_BOOKING); + } + + private RoutingRequest buildRequest(String from, String to, String date, String time, boolean arriveBy, + int maxWalkDistance, double callAndRideReluctance, double walkReluctance, + double waitAtBeginningFactor, int transferPenalty, + boolean ignoreDrtAdvanceMinBooking) { + RoutingRequest options = new RoutingRequest(); + options.setArriveBy(arriveBy); + // defaults in vermont router-config.json + options.setMaxWalkDistance(maxWalkDistance); + options.flexCallAndRideReluctance = callAndRideReluctance; + options.walkReluctance = walkReluctance; + options.waitAtBeginningFactor = waitAtBeginningFactor; + options.transferPenalty = transferPenalty; + + // for testing + options.flexIgnoreDrtAdvanceBookMin = ignoreDrtAdvanceMinBooking; + + options.setDateTime(date, time, graph.getTimeZone()); + options.setFromString(from); + options.setToString(to); + options.setRoutingContext(graph); + + return options; + } + + + private GraphPath getPathToDestination(RoutingRequest options) { + Router router = new Router("default", graph); + return new GraphPathFinder(router).getPaths(options).get(0); + } + + private void checkFare(GraphPath path) { + // test fare calculated correctly + FareService fareService = graph.getService(FareService.class); + Fare cost = fareService.getCost(path); + assertNotNull(cost); + assertEquals("920", cost.getDetails(Fare.FareType.regular).iterator().next().fareId.getId()); + } + + private void assertDateEquals(long timeSeconds, String date, String time, TimeZone timeZone) { + assertEquals(DateUtils.toDate(date, time, timeZone), new Date(timeSeconds * 1000)); + } +} diff --git a/src/test/java/org/opentripplanner/routing/graph/GraphIndexTest.java b/src/test/java/org/opentripplanner/routing/graph/GraphIndexTest.java index 15e62be2215..1ead0ca6e1a 100644 --- a/src/test/java/org/opentripplanner/routing/graph/GraphIndexTest.java +++ b/src/test/java/org/opentripplanner/routing/graph/GraphIndexTest.java @@ -11,8 +11,8 @@ import org.opentripplanner.routing.edgetype.TripPattern; import org.opentripplanner.routing.vertextype.TransitStop; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; import java.util.List; import java.util.Map; diff --git a/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java b/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java new file mode 100644 index 00000000000..28653fbf652 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java @@ -0,0 +1,124 @@ +package org.opentripplanner.routing.graph; + +import com.conveyal.object_differ.ObjectDiffer; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Polygon; +import org.geotools.util.WeakValueHashMap; +import org.jets3t.service.io.TempFile; +import org.junit.Test; +import org.opentripplanner.ConstantsForTests; +import org.opentripplanner.common.geometry.HashGridSpatialIndex; +import org.opentripplanner.routing.trippattern.Deduplicator; +import org.opentripplanner.routing.vertextype.TransitStation; + +import java.io.File; +import java.util.BitSet; +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertFalse; + + +/** + * Tests that saving a graph and reloading it (round trip through serialization and deserialization) does not corrupt + * the graph, and yields exactly the same data. + * + * We tried several existing libraries to perform the comparison but nothing did exactly what we needed in a way that + * we could control precisely. + * + * Created by abyrd on 2018-10-26 + */ +public class GraphSerializationTest { + + /** + * Tests that saving a Graph to disk and reloading it results in a separate but semantically identical Graph. + */ + @Test + public void testRoundTrip () throws Exception { + // This graph does not make an ideal test because it doesn't have any street data. + // TODO switch to another graph that has both GTFS and OSM data + Graph originalGraph = ConstantsForTests.getInstance().getPortlandGraph(); + // Remove the transit stations, which have no edges and won't survive serialization. + List transitVertices = originalGraph.getVertices().stream() + .filter(v -> v instanceof TransitStation).collect(Collectors.toList()); + transitVertices.forEach(originalGraph::remove); + originalGraph.index(true); + // The cached timezone in the graph is transient and lazy-initialized. + // Previous tests may have caused a timezone to be cached. + originalGraph.clearTimeZone(); + // Now round-trip the graph through serialization. + File tempFile = TempFile.createTempFile("graph", "pdx"); + originalGraph.save(tempFile); + Graph copiedGraph1 = Graph.load(tempFile); + assertNoDifferences(originalGraph, copiedGraph1); + Graph copiedGraph2 = Graph.load(tempFile); + assertNoDifferences(copiedGraph1, copiedGraph2); + } + + // Ideally we'd also test comparing two separate but identical complex graphs, built separately from the same inputs. + // A test that builds the same graph twice will currently fail for the following reasons: + // There is global state in Vertex.index and the feeds IDs that mean if you build the same graph twice the feed IDs + // and vertex index numbers will all be unique. This is however evidence that the diff tool is detecting changes in + // the graph contents. On the other hand deserializing the same graph twice or doing a round trip through + // serialization and deserialization should produce identical graphs. + + /** + * Test comparison of two references to the same graph. This should obviously yield no differences at all, + * and allows us to perform a very deep comparison of almost the entire object graph because there are no problems + * with lists being reordered, transient indexes being rebuilt, etc. The ObjectDiffer supports such comparisons + * of identical objects with a special switch specifically for testing. + * + * This is as much a test of the ObjectDiffer itself as of OpenTripPlanner serialization. It is situated here + * instead of in the same package as ObjectDiffer so it has access to the OpenTripPlanner classes, which provide a + * suitably complex tangle of fields and references for exercising all the differ's capabilities. + */ + @Test + public void compareGraphToItself () { + // This graph does not make an ideal test because it doesn't have any street data. + // TODO switch to another graph that has both GTFS and OSM data + Graph originalGraph = ConstantsForTests.getInstance().getPortlandGraph(); + originalGraph.index(false); + // We can exclude relatively few classes here, because the object trees are of course perfectly identical. + // We do skip edge lists - otherwise we trigger a depth-first search of the graph causing a stack overflow. + // We also skip some deeply buried weak-value hash maps, which refuse to tell you what their keys are. + ObjectDiffer objectDiffer = new ObjectDiffer(); + objectDiffer.ignoreFields("incoming", "outgoing"); + objectDiffer.useEquals(BitSet.class, LineString.class, Polygon.class); + // ThreadPoolExecutor contains a weak reference to a very deep chain of Finalizer instances. + objectDiffer.ignoreClasses(WeakValueHashMap.class, ThreadPoolExecutor.class); + // This setting is critical to perform a deep test of an object against itself. + objectDiffer.enableComparingIdenticalObjects(); + objectDiffer.compareTwoObjects(originalGraph, originalGraph); + assertFalse(objectDiffer.hasDifferences()); + } + + + /** + * Compare two separate essentially empty graphs. + */ + @Test + public void testEmptyGraphs() { + Graph graph1 = new Graph(); + Graph graph2 = new Graph(); + assertNoDifferences(graph1, graph2); + } + + private static void assertNoDifferences (Graph g1, Graph g2) { + // Make some exclusions because some classes are inherently transient or contain unordered lists we can't yet compare. + ObjectDiffer objectDiffer = new ObjectDiffer(); + // Skip incoming and outgoing edge lists. These are unordered lists which will not compare properly. + // The edges themselves will be compared via another field, and the edge lists are reconstructed after deserialization. + // Some tests re-build the graph which will result in build times different by as little as a few milliseconds. + objectDiffer.ignoreFields("incoming", "outgoing", "buildTime"); + objectDiffer.useEquals(BitSet.class, LineString.class, Polygon.class); + // HashGridSpatialIndex contains unordered lists in its bins. This is rebuilt after deserialization anyway. + // The deduplicator in the loaded graph will be empty, because it is transient and only fills up when items + // are deduplicated. + objectDiffer.ignoreClasses(HashGridSpatialIndex.class, ThreadPoolExecutor.class, Deduplicator.class); + objectDiffer.compareTwoObjects(g1, g2); + // Print differences before assertion so we can see what went wrong. + assertFalse(objectDiffer.hasDifferences()); + } + +} diff --git a/src/test/java/org/opentripplanner/routing/graph/SimpleConcreteEdge.java b/src/test/java/org/opentripplanner/routing/graph/SimpleConcreteEdge.java index c6516443049..26d8c643c6a 100644 --- a/src/test/java/org/opentripplanner/routing/graph/SimpleConcreteEdge.java +++ b/src/test/java/org/opentripplanner/routing/graph/SimpleConcreteEdge.java @@ -5,7 +5,7 @@ import org.opentripplanner.routing.core.StateEditor; import org.opentripplanner.routing.core.TraverseMode; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; public class SimpleConcreteEdge extends Edge { diff --git a/src/test/java/org/opentripplanner/routing/graph/TemporaryConcreteEdge.java b/src/test/java/org/opentripplanner/routing/graph/TemporaryConcreteEdge.java index ad8d8fdbdec..5544c1aebf3 100644 --- a/src/test/java/org/opentripplanner/routing/graph/TemporaryConcreteEdge.java +++ b/src/test/java/org/opentripplanner/routing/graph/TemporaryConcreteEdge.java @@ -5,7 +5,7 @@ import org.opentripplanner.routing.core.StateEditor; import org.opentripplanner.routing.core.TraverseMode; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.LineString; import java.util.Locale; import org.opentripplanner.routing.edgetype.TemporaryEdge; import org.opentripplanner.routing.vertextype.TemporaryVertex; diff --git a/src/test/java/org/opentripplanner/routing/impl/GraphServiceTest.java b/src/test/java/org/opentripplanner/routing/impl/GraphServiceTest.java index 56a0f97a809..9044a1c903b 100644 --- a/src/test/java/org/opentripplanner/routing/impl/GraphServiceTest.java +++ b/src/test/java/org/opentripplanner/routing/impl/GraphServiceTest.java @@ -41,7 +41,7 @@ protected void setUp() throws IOException { // Create an empty graph and it's serialized form emptyGraph = new Graph(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - emptyGraph.save(new ObjectOutputStream(baos)); + emptyGraph.save(baos); emptyGraphData = baos.toByteArray(); // Create a small graph with 2 vertices and one edge and it's serialized form @@ -50,7 +50,7 @@ protected void setUp() throws IOException { StreetVertex v2 = new IntersectionVertex(smallGraph, "v2", 0, 0.1); new StreetEdge(v1, v2, null, "v1v2", 11000, StreetTraversalPermission.PEDESTRIAN, false); baos = new ByteArrayOutputStream(); - smallGraph.save(new ObjectOutputStream(baos)); + smallGraph.save(baos); smallGraphData = baos.toByteArray(); } diff --git a/src/test/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImplTest.java b/src/test/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImplTest.java index 918b35f333d..fbb5161f2ae 100644 --- a/src/test/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImplTest.java +++ b/src/test/java/org/opentripplanner/routing/impl/OnBoardDepartServiceImplTest.java @@ -1,10 +1,10 @@ package org.opentripplanner.routing.impl; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.CoordinateSequenceFactory; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequenceFactory; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.mock; diff --git a/src/test/java/org/opentripplanner/routing/trippattern/DrtTravelTimeTest.java b/src/test/java/org/opentripplanner/routing/trippattern/DrtTravelTimeTest.java new file mode 100644 index 00000000000..48349e17f63 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/trippattern/DrtTravelTimeTest.java @@ -0,0 +1,59 @@ +package org.opentripplanner.routing.trippattern; + +import junit.framework.TestCase; + +public class DrtTravelTimeTest extends TestCase { + + public void testConstant() { + DrtTravelTime tt = DrtTravelTime.fromSpec("20"); + assertEquals(0d, tt.getCoefficient()); + assertEquals(1200d, tt.getConstant()); + assertEquals(1200d, tt.process(100)); + } + + public void testCoefficient() { + DrtTravelTime tt = DrtTravelTime.fromSpec("3t"); + assertEquals(3d, tt.getCoefficient()); + assertEquals(0d, tt.getConstant()); + assertEquals(180d, tt.process(60d)); + } + + public void testArithmeticFunction() { + DrtTravelTime tt = DrtTravelTime.fromSpec("2.5t+5"); + assertEquals(2.5, tt.getCoefficient()); + assertEquals(300d, tt.getConstant()); + assertEquals(1800d, tt.process(600)); + } + + public void testTwoDigitConstant() { + DrtTravelTime tt = DrtTravelTime.fromSpec("1t+12"); + assertEquals(1d, tt.getCoefficient()); + assertEquals(720d, tt.getConstant()); + assertEquals(1320d, tt.process(600)); + } + + public void testDecimalsEverywhere() { + DrtTravelTime tt = DrtTravelTime.fromSpec("3.0t+5.00"); + assertEquals(3.0, tt.getCoefficient()); + assertEquals(300.0, tt.getConstant()); + assertEquals(2100d, tt.process(600)); + } + + public void testLarge() { + DrtTravelTime tt = DrtTravelTime.fromSpec("2880.0"); + assertEquals(0.0, tt.getCoefficient()); + assertEquals(172800d, tt.getConstant()); + assertEquals(172800d, tt.process(20)); + assertEquals(172800d, tt.process(88)); + } + + public void testBadSpec() { + try { + DrtTravelTime.fromSpec("not to spec"); + fail( "Missing exception"); + } catch(IllegalArgumentException e) { + assertEquals( e.getMessage(), DrtTravelTime.ERROR_MSG); + } + } + +} diff --git a/src/test/java/org/opentripplanner/routing/trippattern/FrequencyEntryTest.java b/src/test/java/org/opentripplanner/routing/trippattern/FrequencyEntryTest.java new file mode 100644 index 00000000000..5f9b167f8e9 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/trippattern/FrequencyEntryTest.java @@ -0,0 +1,124 @@ +/* This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +package org.opentripplanner.routing.trippattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.opentripplanner.model.FeedScopedId; +import org.opentripplanner.model.Frequency; +import org.opentripplanner.model.Stop; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.model.Trip; + +public class FrequencyEntryTest { + + private static final int STOP_NUM = 8; + private static final TripTimes tripTimes; + + static { + Trip trip = new Trip(); + trip.setId(new FeedScopedId("agency", "testtrip")); + + List stopTimes = new ArrayList(); + + int time = 0; + for(int i = 0; i < STOP_NUM; ++i) { + FeedScopedId id = new FeedScopedId("agency", i+""); + + Stop stop= new Stop(); + stop.setId(id); + + StopTime stopTime = new StopTime(); + stopTime.setStop(stop); + stopTime.setArrivalTime(time); + if(i != 0 && i != STOP_NUM - 1) { + time += 10; + } + stopTime.setDepartureTime(time); + time += 90; + stopTime.setStopSequence(i); + stopTimes.add(stopTime); + } + + tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + } + + private static FrequencyEntry make(int startTime, int endTime, int headwaySecs, boolean exact) { + Frequency f = new Frequency(); + f.setStartTime(startTime); + f.setEndTime(endTime); + f.setHeadwaySecs(headwaySecs); + f.setExactTimes(exact ? 1 : 0); + + return new FrequencyEntry(f, tripTimes); + } + + @Test + public void testExactFrequencyProperEnd() { + FrequencyEntry fe = make(100000, 150000, 100, true); + assertEquals(149900, fe.nextDepartureTime(0, 149900)); + assertEquals(-1, fe.nextDepartureTime(0, 150000)); + } + + @Test + public void testExactFrequencyStopOffset() { + FrequencyEntry fe = make(100000, 150001, 100, true); + + // testing first trip departure + assertEquals(100000, fe.nextDepartureTime(0, 100000)); // first stop, on begin + assertEquals(100500, fe.nextDepartureTime(5, 100000)); // 6th stop, before begin + + // testing last trip departure + assertEquals(150000, fe.nextDepartureTime(0, 150000)); // 1st stop, on end + assertEquals(-1, fe.nextDepartureTime(0, 150100)); // 1st stop, after end + assertEquals(150100, fe.nextDepartureTime(1, 150100)); // 2nd stop, on end + assertEquals(150500, fe.nextDepartureTime(5, 150500)); // 6th stop, on end + assertEquals(-1, fe.nextDepartureTime(5, 150600)); // 6th stop, after end + + // testing first trip arrival + assertEquals(-1, fe.prevArrivalTime(4, 100300)); // 5th stop, before begin + assertEquals(100390, fe.prevArrivalTime(4, 100400)); // 5th stop, after begin + assertEquals(-1, fe.prevArrivalTime(7, 100600)); // 8th stop, before begin + assertEquals(100690, fe.prevArrivalTime(7, 100700)); // 8th stop, after begin + + // testing last trip arrival + assertEquals(150390, fe.prevArrivalTime(4, 150700)); // 5th stop + assertEquals(150690, fe.prevArrivalTime(7, 150700)); // 8th stop, on end + assertEquals(150690, fe.prevArrivalTime(7, 150750)); // 8th stop, after end + } + + @Test + public void testInexactFrequencyStopOffset() { + FrequencyEntry fe = make(100000, 150000, 100, false); + + // testing last trip departure + assertEquals(149900, fe.nextDepartureTime(0, 149800)); // 1st stop, before end + assertEquals(-1, fe.nextDepartureTime(0, 150100)); // 1st stop, after end + assertEquals(150400, fe.nextDepartureTime(5, 150300)); // 6th stop, before end + assertEquals(-1, fe.nextDepartureTime(5, 150600)); // 6th stop, after end + + // testing first trip arrival + assertEquals(-1, fe.prevArrivalTime(4, 100300)); // 5th stop, before begin + assertEquals(100400, fe.prevArrivalTime(4, 100500)); // 5th stop, after begin + assertEquals(-1, fe.prevArrivalTime(7, 100600)); // 8th stop, before begin + assertEquals(100700, fe.prevArrivalTime(7, 100800)); // 8th stop, after begin + } +} diff --git a/src/test/java/org/opentripplanner/routing/trippattern/TripTimesTest.java b/src/test/java/org/opentripplanner/routing/trippattern/TripTimesTest.java index 390534ad222..fde57aedd01 100644 --- a/src/test/java/org/opentripplanner/routing/trippattern/TripTimesTest.java +++ b/src/test/java/org/opentripplanner/routing/trippattern/TripTimesTest.java @@ -9,6 +9,7 @@ import java.util.List; import org.junit.Test; +import org.mockito.Matchers; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.Route; import org.opentripplanner.model.Stop; @@ -16,12 +17,15 @@ import org.opentripplanner.model.Trip; import org.opentripplanner.gtfs.BikeAccess; import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.ServiceDay; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.SimpleConcreteVertex; import org.opentripplanner.routing.graph.Vertex; +import static org.mockito.Mockito.*; + public class TripTimesTest { private static final FeedScopedId tripId = new FeedScopedId("agency", "testtrip"); @@ -229,4 +233,80 @@ public void testGetDwellTime() { assertEquals(i, updatedTripTimes.getDwellTime(i)); } } + + @Test + public void testCallAndRideBoardTime() { + // times: 0, 60, 120 + + ServiceDay sd = mock(ServiceDay.class); + when(sd.secondsSinceMidnight(Matchers.anyLong())).thenCallRealMethod(); + int time; + + // time before interval + time = originalTripTimes.getCallAndRideBoardTime(1, 50, 20, sd, false, 0); + assertEquals(60, time); + + // time in interval + time = originalTripTimes.getCallAndRideBoardTime(1, 70, 20, sd, false, 0); + assertEquals(70, time); + + // time would overlap end of interval + time = originalTripTimes.getCallAndRideBoardTime(1, 105, 20, sd, false, 0); + assertTrue(time < 105); + + // time after end of interval + time = originalTripTimes.getCallAndRideBoardTime(1, 125, 20, sd, false, 0); + assertTrue(time < 105); + + // clock time before + time = originalTripTimes.getCallAndRideBoardTime(1, 50, 20, sd, true, 30); + assertEquals(60, time); + + // clock time in interval + time = originalTripTimes.getCallAndRideBoardTime(1, 50, 20, sd, true, 70); + assertEquals(70, time); + + // clock time after interval + time = originalTripTimes.getCallAndRideBoardTime(1, 50, 20, sd, true, 130); + assertTrue(time < 50); + + // clock time would cause overlap + time = originalTripTimes.getCallAndRideBoardTime(1, 50, 20, sd, true, 105); + assertTrue(time < 50); + } + + @Test + public void testCallAndRideAlightTime() { + ServiceDay sd = mock(ServiceDay.class); + when(sd.secondsSinceMidnight(Matchers.anyLong())).thenCallRealMethod(); + int time; + + // time after interval + time = originalTripTimes.getCallAndRideAlightTime(2, 130, 20, sd, false, 0); + assertEquals(120, time); + + // time in interval + time = originalTripTimes.getCallAndRideAlightTime(2, 110, 20, sd, false, 0); + assertEquals(110, time); + + // time in interval, would cause overlap + time = originalTripTimes.getCallAndRideAlightTime(2, 65, 20, sd, false, 0); + assertTrue(time == -1 || time > 65); + + // time after interval + time = originalTripTimes.getCallAndRideAlightTime(2, 55, 20, sd, false, 0); + assertTrue(time == -1 || time > 65); + + // clock time after interval + time = originalTripTimes.getCallAndRideAlightTime(2, 130, 20, sd, true, 130); + assertEquals(-1, time); + + // clock time before board + time = originalTripTimes.getCallAndRideAlightTime(2, 110, 20, sd, true, 85); + assertEquals(110, time); + + // clock time after board + time = originalTripTimes.getCallAndRideAlightTime(2, 110, 20, sd, true, 100); + assertEquals(-1, time); + } } diff --git a/src/test/java/org/opentripplanner/routing/util/TestElevationUtils.java b/src/test/java/org/opentripplanner/routing/util/TestElevationUtils.java index d6d58507af1..48ad681dcb7 100644 --- a/src/test/java/org/opentripplanner/routing/util/TestElevationUtils.java +++ b/src/test/java/org/opentripplanner/routing/util/TestElevationUtils.java @@ -2,9 +2,9 @@ import junit.framework.TestCase; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.CoordinateSequence; -import com.vividsolutions.jts.geom.impl.PackedCoordinateSequenceFactory; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory; public class TestElevationUtils extends TestCase { @@ -27,4 +27,26 @@ public void testLengthMultiplier() { assertEquals(1.00992634231424500668, costs.lengthMultiplier); } + public void testCalculateSlopeWalkEffectiveLengthFactor() { + // 35% should hit the MAX_SLOPE_WALK_EFFECTIVE_LENGTH_FACTOR=3, hence 300m is expected + assertEquals(300.0, ElevationUtils.calculateEffectiveWalkLength(100, 35), 0.1); + + // 10% incline equals 1.42 penalty + assertEquals(141.9, ElevationUtils.calculateEffectiveWalkLength(100, 10), 0.1); + + // Flat is flat, no penalty + assertEquals(120.0, ElevationUtils.calculateEffectiveWalkLength(120, 0)); + + // 5% downhill is the fastest to walk and effective distance only 0.83 * flat distance + assertEquals(83.9, ElevationUtils.calculateEffectiveWalkLength(100, -5), 0.1); + + // 10% downhill is about the same as flat + assertEquals(150.0, ElevationUtils.calculateEffectiveWalkLength(150, -15)); + + // 15% downhill have a penalty of 1.19 + assertEquals(238.2, ElevationUtils.calculateEffectiveWalkLength(200, -30), 0.1); + + // 45% downhill hit the MAX_SLOPE_WALK_EFFECTIVE_LENGTH_FACTOR=3 again + assertEquals(300.0, ElevationUtils.calculateEffectiveWalkLength(100, -45), 0.1); + } } diff --git a/src/test/java/org/opentripplanner/routing/util/elevation/ToblersHikingFunctionTest.java b/src/test/java/org/opentripplanner/routing/util/elevation/ToblersHikingFunctionTest.java new file mode 100644 index 00000000000..bbc16d2dd5b --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/util/elevation/ToblersHikingFunctionTest.java @@ -0,0 +1,65 @@ +package org.opentripplanner.routing.util.elevation; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.opentripplanner.routing.util.elevation.ToblersHikingFunctionTest.TestCase.tc; + +public class ToblersHikingFunctionTest { + + private static final double CUT_OFF_LIMIT = 3.2; + + @Test + public void calculateHorizontalWalkingDistanceMultiplier() { + TestCase[] testCases = { + tc(35, CUT_OFF_LIMIT), + tc(31.4, 3.0), + tc(30, 2.86), + tc(25, 2.40), + tc(20, 2.02), + tc(15, 1.69), + tc(10, 1.42), + tc(5, 1.19), + tc(0, 1.00), + tc(-5, 0.84), + tc(-10, 1.00), + tc(-15, 1.19), + tc(-20, 1.42), + tc(-25, 1.69), + tc(-30, 2.02), + tc(-35, 2.40), + tc(-40, 2.86), + tc(-45, CUT_OFF_LIMIT) + }; + + ToblersHikingFunction f = new ToblersHikingFunction(CUT_OFF_LIMIT); + + for (TestCase it : testCases) { + double distMultiplier = f.calculateHorizontalWalkingDistanceMultiplier(it.dx, it.dh); + assertEquals(it.describe(), it.expected, distMultiplier, 0.01); + } + } + + static class TestCase { + + final double slopeAnglePercentage, dx, dh, expected; + + TestCase(double slopeAnglePercentage, double expected) { + this.slopeAnglePercentage = slopeAnglePercentage; + // Set the horizontal distance to 300 meters + this.dx = 300.0; + // Calculate the height: + this.dh = dx * slopeAnglePercentage / 100.0; + + this.expected = expected; + } + + static TestCase tc(double slopeAngle, double expected) { + return new TestCase(slopeAngle, expected); + } + + String describe() { + return String.format("Multiplier at %.1f%% slope angle with dx %.1f and dh %.1f.", slopeAnglePercentage, dx, dh); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/routing/vertextype/BarrierVertexTest.java b/src/test/java/org/opentripplanner/routing/vertextype/BarrierVertexTest.java index ed23844e846..fa559fa6ca7 100644 --- a/src/test/java/org/opentripplanner/routing/vertextype/BarrierVertexTest.java +++ b/src/test/java/org/opentripplanner/routing/vertextype/BarrierVertexTest.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.vertextype; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import org.junit.Test; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.graph_builder.module.osm.OSMFilter; diff --git a/src/test/java/org/opentripplanner/routing/vertextype/IntersectionVertexTest.java b/src/test/java/org/opentripplanner/routing/vertextype/IntersectionVertexTest.java index 3f67a7a0801..15aa27a2c3d 100644 --- a/src/test/java/org/opentripplanner/routing/vertextype/IntersectionVertexTest.java +++ b/src/test/java/org/opentripplanner/routing/vertextype/IntersectionVertexTest.java @@ -9,8 +9,8 @@ import org.opentripplanner.routing.edgetype.StreetTraversalPermission; import org.opentripplanner.routing.graph.Graph; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.LineString; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; public class IntersectionVertexTest { diff --git a/src/test/java/org/opentripplanner/routing/vertextype/TemporaryVertexMultithreadTest.java b/src/test/java/org/opentripplanner/routing/vertextype/TemporaryVertexMultithreadTest.java new file mode 100644 index 00000000000..fd2aa9f7a7c --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/vertextype/TemporaryVertexMultithreadTest.java @@ -0,0 +1,68 @@ +package org.opentripplanner.routing.vertextype; + +import org.junit.Test; +import org.opentripplanner.routing.core.RoutingRequest; +import org.opentripplanner.routing.core.State; +import org.opentripplanner.routing.edgetype.FreeEdge; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.Vertex; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class TemporaryVertexMultithreadTest { + + private static final double ANY_LOC = 1; + + @Test + public void testTemporaryVertexOnOtherThreadUnreachable() throws ExecutionException, InterruptedException { + Graph graph = new Graph(); + Vertex a = new V(graph, "A"); + Vertex b = new V(graph, "B"); + Vertex c = new TempVertex(graph, "C"); + Vertex d = new V(graph, "D"); + Vertex e = new V(graph, "E"); + + new FreeEdge(a, b); + Edge edgeToTemporaryVertex = new FreeEdge(b, c); + new FreeEdge(c, d); + new FreeEdge(d, e); + RoutingRequest options1 = new RoutingRequest(); + options1.setRoutingContext(graph, a, e); + options1.rctx.temporaryVertices.add(c); + State init1 = new State(b, options1); + assertNotNull(edgeToTemporaryVertex.traverse(init1)); + FutureTask otherThreadState = new FutureTask<>(() -> { + RoutingRequest options2 = new RoutingRequest(); + options2.setRoutingContext(graph, a, e); + State init2 = new State(b, options2); + return edgeToTemporaryVertex.traverse(init2); + }); + new Thread(otherThreadState).start(); + assertNull(otherThreadState.get()); + } + + private class V extends Vertex { + private V(Graph graph, String label) { + super(graph, label, ANY_LOC, ANY_LOC); + } + + @Override public String toString() { + return getLabel(); + } + } + + private class TempVertex extends V implements TemporaryVertex { + private TempVertex(Graph graph, String label) { + super(graph, label); + } + + @Override public boolean isEndVertex() { + throw new IllegalStateException("The `isEndVertex` is not used by dispose logic."); + } + } +} diff --git a/src/test/java/org/opentripplanner/traffic/StreetSpeedSourceTest.java b/src/test/java/org/opentripplanner/traffic/StreetSpeedSourceTest.java deleted file mode 100644 index 59cb72357ae..00000000000 --- a/src/test/java/org/opentripplanner/traffic/StreetSpeedSourceTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.opentripplanner.traffic; - -import com.google.common.collect.Maps; -import junit.framework.TestCase; -import org.junit.Test; -import org.opentripplanner.routing.core.TraverseMode; -import org.opentripplanner.routing.edgetype.StreetEdge; -import org.opentripplanner.routing.edgetype.StreetTraversalPermission; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.vertextype.OsmVertex; - -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.util.Map; - -/** Test that street speed sources get used */ -public class StreetSpeedSourceTest extends TestCase { - @Test - public void testMatching () { - Graph g = new Graph(); - OsmVertex v1 = new OsmVertex(g, "v1", 0, 0, 5l); - OsmVertex v2 = new OsmVertex(g, "v2", 0, 0.01, 6l); - StreetEdge se = new StreetEdge(v1, v2, null, "test", 1000, StreetTraversalPermission.CAR, false); - se.wayId = 10; - - // create a speed sample - SegmentSpeedSample s = getSpeedSample(); - - Map speeds = Maps.newHashMap(); - Segment seg = new Segment(10l, 5l, 6l); - speeds.put(seg, s); - - g.streetSpeedSource = new StreetSpeedSnapshotSource(); - g.streetSpeedSource.setSnapshot(new StreetSpeedSnapshot(speeds)); - - // confirm that we get the correct speeds. - // This also implicitly tests encoding/decoding - - OffsetDateTime odt = OffsetDateTime.of(2015, 6, 1, 8, 5, 0, 0, ZoneOffset.UTC); - - StreetSpeedSnapshot snap = g.streetSpeedSource.getSnapshot(); - - double monday8am = snap.getSpeed(se, TraverseMode.CAR, odt.toInstant().toEpochMilli()); - // no data: should use average - assertEquals(1.33, monday8am, 0.1); - - odt = odt.plusHours(1); - - double monday9am = snap.getSpeed(se, TraverseMode.CAR, odt.toInstant().toEpochMilli()); - assertEquals(6.1, monday9am, 0.1); - - odt = odt.plusHours(1); - - double monday10am = snap.getSpeed(se, TraverseMode.CAR, odt.toInstant().toEpochMilli()); - assertEquals(33.3, monday10am, 0.1); - - se.wayId = 102; - double wrongStreet = snap.getSpeed(se, TraverseMode.CAR, odt.toInstant().toEpochMilli()); - assertTrue(Double.isNaN(wrongStreet)); - } - - @Test - public void testConcurrency () { - Graph g = new Graph(); - - StreetSpeedSnapshotSource ssss = new StreetSpeedSnapshotSource(); - - OsmVertex v1 = new OsmVertex(g, "v1", 0, 0, 5l); - OsmVertex v2 = new OsmVertex(g, "v2", 0, 0.01, 6l); - StreetEdge se = new StreetEdge(v1, v2, null, "test", 1000, StreetTraversalPermission.CAR, false); - se.wayId = 10; - - Map ss2 = Maps.newHashMap(); - Segment seg = new Segment(10l, 5l, 6l); - ss2.put(seg, getSpeedSample()); - StreetSpeedSnapshot ssOrig = new StreetSpeedSnapshot(ss2); - ssss.setSnapshot(ssOrig); - StreetSpeedSnapshot snap = ssss.getSnapshot(); - assertEquals(ssOrig, snap); - - // should be match - assertFalse(Double.isNaN(snap.getSpeed(se, TraverseMode.CAR, System.currentTimeMillis()))); - - // should not have changed - assertEquals(snap, ssss.getSnapshot()); - - Map ss1 = Maps.newHashMap(); - seg = new Segment(10l, 4l, 6l); - ss1.put(seg, getSpeedSample()); - StreetSpeedSnapshot ssNew = new StreetSpeedSnapshot(ss1); - ssss.setSnapshot(ssNew); - - snap = ssss.getSnapshot(); - assertEquals(ssNew, snap); - - // should be no match; the segment in the index does not match the street edge - assertTrue(Double.isNaN(snap.getSpeed(se, TraverseMode.CAR, System.currentTimeMillis()))); - } - - /** Make a speed sample */ - private SegmentSpeedSample getSpeedSample() { - double[] hourBins = new double[7 * 24]; - for (int i = 0; i < hourBins.length; i++) { - if (i == 8) - hourBins[i] = Double.NaN; - else if (i == 9) - // ~20km/h - hourBins[i] = 6.1; - else - // ~120 km/h - hourBins[i] = 33.3; - - } - - // ~4km/h - double avg = 1.33; - - return new SegmentSpeedSample(avg, hourBins); - } -} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/updater/car_rental/TestCar2GoCarRentalDataSource.java b/src/test/java/org/opentripplanner/updater/car_rental/TestCar2GoCarRentalDataSource.java index f27218c6d80..d5cd74578ef 100644 --- a/src/test/java/org/opentripplanner/updater/car_rental/TestCar2GoCarRentalDataSource.java +++ b/src/test/java/org/opentripplanner/updater/car_rental/TestCar2GoCarRentalDataSource.java @@ -15,11 +15,11 @@ the License, or (at your option) any later version. import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; import junit.framework.TestCase; import org.junit.BeforeClass; import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; import org.opentripplanner.routing.car_rental.CarFuelType; import org.opentripplanner.routing.car_rental.CarRentalRegion; import org.opentripplanner.routing.car_rental.CarRentalStation; diff --git a/src/test/java/org/opentripplanner/util/TestPolylineEncoder.java b/src/test/java/org/opentripplanner/util/TestPolylineEncoder.java index 0ce82b5d056..992e6b293cc 100644 --- a/src/test/java/org/opentripplanner/util/TestPolylineEncoder.java +++ b/src/test/java/org/opentripplanner/util/TestPolylineEncoder.java @@ -5,7 +5,7 @@ import org.opentripplanner.util.model.EncodedPolylineBean; -import com.vividsolutions.jts.geom.Coordinate; +import org.locationtech.jts.geom.Coordinate; import junit.framework.TestCase; diff --git a/src/test/java/org/opentripplanner/util/TranslatedStringTest.java b/src/test/java/org/opentripplanner/util/TranslatedStringTest.java index d853d113322..53bc218cfc7 100644 --- a/src/test/java/org/opentripplanner/util/TranslatedStringTest.java +++ b/src/test/java/org/opentripplanner/util/TranslatedStringTest.java @@ -7,6 +7,15 @@ public class TranslatedStringTest extends TestCase { + public void testGetI18NStringIsEmpty() { + try { + TranslatedString.getI18NString(new HashMap<>()); + fail("Expected an IllegalArgumentException to be thrown"); + } catch (IllegalArgumentException illigalArgumentException) { + assertEquals(illigalArgumentException.getMessage(), "Map of languages and translations is empty."); + } + } + public void testGetI18NString() throws Exception { HashMap translations = new HashMap<>(); diff --git a/src/test/resources/org/opentripplanner/graph_builder/module/addPerpendicularRoutes.gtfs.zip b/src/test/resources/org/opentripplanner/graph_builder/module/addPerpendicularRoutes.gtfs.zip new file mode 100644 index 00000000000..c1feca1d88f Binary files /dev/null and b/src/test/resources/org/opentripplanner/graph_builder/module/addPerpendicularRoutes.gtfs.zip differ diff --git a/src/test/resources/org/opentripplanner/graph_builder/module/addTransitMultipleLines.gtfs.zip b/src/test/resources/org/opentripplanner/graph_builder/module/addTransitMultipleLines.gtfs.zip new file mode 100644 index 00000000000..e0a3435c569 Binary files /dev/null and b/src/test/resources/org/opentripplanner/graph_builder/module/addTransitMultipleLines.gtfs.zip differ diff --git a/src/test/resources/vermont/ruralcommunity-flex-vt-us.zip b/src/test/resources/vermont/ruralcommunity-flex-vt-us.zip new file mode 100644 index 00000000000..7aadfae1bef Binary files /dev/null and b/src/test/resources/vermont/ruralcommunity-flex-vt-us.zip differ diff --git a/src/test/resources/vermont/vermont-rct.osm.pbf b/src/test/resources/vermont/vermont-rct.osm.pbf new file mode 100644 index 00000000000..2ca491bca57 Binary files /dev/null and b/src/test/resources/vermont/vermont-rct.osm.pbf differ