Skip to content

Commit 6ac9e45

Browse files
committed
Merge remote-tracking branch 'gp2/dev' into gp2
2 parents 6ea5083 + fc97c79 commit 6ac9e45

15 files changed

Lines changed: 614 additions & 3 deletions

File tree

.dockerignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
**/.classpath
2+
**/.dockerignore
3+
**/.env
4+
**/.git
5+
**/.gitignore
6+
**/.project
7+
**/.settings
8+
**/.toolstarget
9+
**/.vs
10+
**/.vscode
11+
**/*.*proj.user
12+
**/*.dbmdl
13+
**/*.jfm
14+
**/bin
15+
**/charts
16+
**/docker-compose*
17+
**/compose*
18+
**/Dockerfile*
19+
**/node_modules
20+
**/npm-debug.log
21+
**/obj
22+
**/secrets.dev.yaml
23+
**/values.dev.yaml
24+
LICENSE
25+
README.md

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.vscode
2+
13
.gradle/
24
/build/
35
/out/

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,59 @@
1+
# Conveyal R5 Routing Engine Greenpaths2 edit
2+
3+
## Greenpaths2: bi-objective custom cost exposure routing
4+
This project has made changes to the source forked r5 and implemented support for custom cost based bi-objective (exposure) routing which is used in Greenpaths2 -tool. The tool is part of projects: GREENTRAVEL, Urban airquality 2.0 and Roope Heinonen's masters thesis (University of Helsinki, Geography).
5+
6+
### Brief overview of Greenpaths2's -tool
7+
R5 is used for its superior routing efficiency, existing infrastructure for custom costs and previous knowledge and implementation of Python wrapper [r5py](https://github.com/r5py/r5py). Greenpaths2 uses the r5 via r5py, which is a tool written in Python which accesses Java using JPype. All the other processes e.g. calculating the custom costs per edge should be done in Python, so the r5 adresses only the heavy lifting for the routing and r5py works as the interface in Python for accessing r5. R5py, as r5, also has most of the infrastructure in place for implementing the bi-objective custom cost routing, but some minor changes are needed in order to be able to utilize this customized version of r5.
8+
9+
The preprocessing i.e. calculating the actual custom costs per OsmId and the analyses i.e. producing the exposure statistics for routes for scientific and more general public GUI are calculated in the navite Greenpaths2 logic.
10+
11+
So the general architecture's tlds;
12+
1) modified r5 java code which has support for custom costs and getting OsmId's for exposure purposes
13+
2) modified r5py which has support for the r5 changes and other Greenpaths2 needs
14+
3) Greenpaths preprocessing module which produces the X custom costs per edge and handles input datas
15+
4) analysis module which then calculates and processes the results derived from r5py
16+
17+
### Details on the implementation
18+
19+
The custom cost functionality is created by both, using the existing infrastructure already found in r5 and introducing new classes which are increasing the base capabilities. Most of the major changes can be found from the codebase using comment "GP2 edit:" as the key search condition. The implementations can be split in to two main categories: class and logic. The classes are newly created and they serve as the containers for the needed components and their logic for the bi-objective custom cost implementation. The logic changes are generally speaking the implementation of these classes and their methods withing the r5 base source code. Here they are by file (ordered by: category, alphabetical order):
20+
21+
## CLASS
22+
23+
### CustomCost.java
24+
Used to isolate custom cost related logic in its own class. Currently has functionality for enabling OsmId fetching from TravelTimeComputer (without public transport) router state i.e. per OD (origin-destination) point pair goes through each edge, yields a list of all OsmIds traversed in the OD-path.
25+
26+
### CustomCostField.java
27+
Utilizes existing infrastructure: CostField Interface, which is already used in custom costs e.g. Elevation and Sun -costs. The most important method is "additionalTraversalTimeSeconds" which is used in the MultistageTraversalTimeCalculator's traversalTimeSeconds which is used to calculate the traversal times per edge. This classes "additionalTraversalTimeSeconds" implements general custom additional cost functionality which can be flexibly used in the bi-objective routing.
28+
29+
### CustomCostTest.java
30+
As the name implies, this file has the tests for the custom cost related components.
31+
32+
33+
## LOGIC
34+
35+
### OneOriginResult.java
36+
Attribute "public final List<List<Long>> osmIdResults" is added to hold the optional OsmIds gathered during custom cost routing. An overload constructor is implemented to also support the normal routing usecase of routing without custom costs.
37+
38+
### TravelTimeComputer.java
39+
A code block is added which checks if the current network has custom costs, and if so, proceed to getting their OsmIds. If the network doesn't have custom costs, just ignore previously mentioned block and continue.
40+
41+
### TravelTimeReducer.java
42+
Has utility functions added for setting and returning the OneOriginResults possibly enriched with the OsmIds data.
43+
44+
### StreetEdgeInfo.java
45+
Has attribute "public Long edgeOsmId" added to the class, this way it's possible to assign an OsmId for the edge in StreetSegment. This is used in the r5py in DetailedItineraries for one-to-one routing.
46+
47+
### StreetSegment.java
48+
Assigns a edgeOsmId for the StreetEdgeInfo instances during the creation and population process.
49+
50+
### StreetSegmentTest.java
51+
Tests for getting the osmId. Uses .json dummy data where the edgeOsmId's are also added.
52+
53+
154
# Conveyal R5 Routing Engine
255

56+
357
## R5: Rapid Realistic Routing on Real-world and Reimagined networks
458
R5 is the routing engine for [Conveyal](https://www.conveyal.com/learn), a web-based system that allows users to create transportation scenarios and evaluate them in terms of cumulative opportunities accessibility indicators. See the [Conveyal user manual](https://docs.conveyal.com/) for more information.
559

src/main/java/com/conveyal/r5/OneOriginResult.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.conveyal.r5;
22

3+
import java.util.List;
4+
35
import com.conveyal.r5.analyst.AccessibilityResult;
46
import com.conveyal.r5.analyst.cluster.PathResult;
57
import com.conveyal.r5.analyst.cluster.TravelTimeResult;
@@ -20,10 +22,20 @@ public class OneOriginResult {
2022

2123
public final PathResult paths;
2224

25+
/* GP2 edit: add this attribute to save OsmIdResults */
26+
public final List<List<Long>> osmIdResults;
27+
28+
/* GP2 edit: add default null OsmIdResults */
2329
public OneOriginResult(TravelTimeResult travelTimes, AccessibilityResult accessibility, PathResult paths) {
30+
this(travelTimes, accessibility, paths, null);
31+
}
32+
33+
/* GP2 edit: add this overload constructor to handle optional osmIdResults */
34+
public OneOriginResult(TravelTimeResult travelTimes, AccessibilityResult accessibility, PathResult paths, List<List<Long>> osmIdResults) {
2435
this.travelTimes = travelTimes;
2536
this.accessibility = accessibility;
2637
this.paths = paths;
38+
this.osmIdResults = osmIdResults;
2739
}
2840

2941
}

src/main/java/com/conveyal/r5/analyst/TravelTimeComputer.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.conveyal.r5.analyst;
22

3+
import com.conveyal.analysis.datasource.DataSourceException;
34
import com.conveyal.r5.OneOriginResult;
45
import com.conveyal.r5.analyst.cluster.AnalysisWorkerTask;
56
import com.conveyal.r5.analyst.cluster.PathWriter;
@@ -14,6 +15,7 @@
1415
import com.conveyal.r5.profile.McRaptorSuboptimalPathProfileRouter;
1516
import com.conveyal.r5.profile.PerTargetPropagater;
1617
import com.conveyal.r5.profile.StreetMode;
18+
import com.conveyal.r5.rastercost.CustomCost;
1719
import com.conveyal.r5.streets.LinkedPointSet;
1820
import com.conveyal.r5.streets.PointSetTimes;
1921
import com.conveyal.r5.streets.Split;
@@ -25,6 +27,7 @@
2527
import org.slf4j.LoggerFactory;
2628

2729
import java.util.EnumSet;
30+
import java.util.List;
2831
import java.util.function.IntFunction;
2932
import java.util.stream.Collectors;
3033

@@ -234,6 +237,19 @@ public OneOriginResult computeTravelTimes() {
234237
}
235238
nonTransitTravelTimesToDestinations = PointSetTimes.minMerge(nonTransitTravelTimesToDestinations, pointSetTimes);
236239
}
240+
241+
// custom cost related code for getting osmids from router state
242+
/* GP2 edit: add this OsmId related code block */
243+
{
244+
// only get osmids if the streetlayer has cost fields
245+
if (network.streetLayer.edgeStore.costFields != null) {
246+
List<List<Long>> osmIdResults = CustomCost.getOsmIdsFromRouterState(nonTransitTravelTimesToDestinations, sr, network);
247+
if (osmIdResults == null) {
248+
throw new DataSourceException("No Destinations or could not get osmId's from path while streetlayer has cost fields");
249+
}
250+
travelTimeReducer.setOsmIdsResult(osmIdResults);
251+
}
252+
}
237253
}
238254

239255
// Handle park+ride, a mode represented in the request LegMode but not in the internal StreetMode.
@@ -268,7 +284,7 @@ public OneOriginResult computeTravelTimes() {
268284
LOG.info("Origin point was outside the street network. Skipping routing and propagation, and returning default result.");
269285
return travelTimeReducer.finish();
270286
}
271-
287+
272288
// Short circuit unnecessary transit routing: If the origin was linked to a road, but no transit stations
273289
// were reached, return the non-transit grid as the final result.
274290
if (request.transitModes.isEmpty() || bestAccessOptions.streetTimesAndModes.isEmpty()) {

src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.slf4j.LoggerFactory;
1818

1919
import java.util.Arrays;
20+
import java.util.List;
2021

2122
import static com.conveyal.r5.common.Util.notNullOrEmpty;
2223
import static com.conveyal.r5.profile.FastRaptorWorker.UNREACHED;
@@ -77,6 +78,13 @@ public class TravelTimeReducer {
7778
/** Provides a weighting factor for opportunities at a given travel time. */
7879
private final DecayFunction decayFunction;
7980

81+
/**
82+
* Store osmIds traversed in the whole routing
83+
* One inner list of osmIds for each path to destination
84+
*/
85+
/* GP2 edit: add this attribute for osmIdsResult */
86+
private List<List<Long>> osmIdsResult;
87+
8088
/**
8189
* Reduce travel time values to requested summary outputs for each origin. The type of output (a single
8290
* cumulative opportunity accessibility value per origin, or selected percentiles of travel times to all
@@ -321,15 +329,22 @@ private int convertToMinutes (int timeSeconds) {
321329
}
322330
}
323331

332+
/** setter for OsmIdResults */
333+
/* GP2 edit: add this setter */
334+
public void setOsmIdsResult(List<List<Long>> osmIdResult) {
335+
this.osmIdsResult = osmIdResult;
336+
}
337+
324338
/**
325339
* This is the primary way to create a OneOriginResult and end the processing.
326340
* Some alternate code paths exist for TAUI site generation and testing, but this handles all other cases.
327341
* For example, if no travel times to destinations have been streamed in by calling recordTravelTimesForTarget, the
328342
* TimeGrid will have a buffer full of UNREACHED. This allows shortcutting around routing and propagation when the
329343
* origin point is not connected to the street network.
330344
*/
345+
/* GP2 edit: add osmIdsResult to OneOriginResult */
331346
public OneOriginResult finish () {
332-
return new OneOriginResult(travelTimeResult, accessibilityResult, pathResult);
347+
return new OneOriginResult(travelTimeResult, accessibilityResult, pathResult, osmIdsResult);
333348
}
334349

335350
/**

src/main/java/com/conveyal/r5/api/util/StreetEdgeInfo.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ public class StreetEdgeInfo {
2323
*/
2424
public Integer edgeId;
2525

26+
// edge's OsmId
27+
/* GP2 edit: add this attribute for saving the osmid
28+
* this is used in point-to-point detailed iteneraries routing in GP2
29+
*/
30+
public Long edgeOsmId;
31+
2632
/**
2733
* Distance of driving on these edge (milimeters)
2834
* @notnull
@@ -64,10 +70,12 @@ public void setAbsoluteDirection(double thisAngle) {
6470
absoluteDirection = AbsoluteDirection.values()[octant];
6571
}
6672

73+
/* GP2 edit: add printing edgeOsmId */
6774
@Override
6875
public String toString() {
6976
String sb = "StreetEdgeInfo{" + "edgeId=" + edgeId +
70-
", distance=" + distance +
77+
", distance=" + distance +
78+
", osmId=" + edgeOsmId +
7179
", geometry=" + geometry +
7280
", mode=" + mode +
7381
", streetName='" + streetName + '\'' +

src/main/java/com/conveyal/r5/api/util/StreetSegment.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public StreetSegment(StreetPath path, LegMode mode, StreetLayer streetLayer) {
8787
EdgeStore.Edge edge = path.getEdge(edgeIdx);
8888
StreetEdgeInfo streetEdgeInfo = new StreetEdgeInfo();
8989
streetEdgeInfo.edgeId = edgeIdx;
90+
/* GP2 edit: add osmid to the streetEdgeInfo, used in detailed iteneraries*/
91+
streetEdgeInfo.edgeOsmId = edge.getOSMID();
9092
streetEdgeInfo.geometry = edge.getGeometry();
9193
streetEdgeInfo.streetName = streetLayer.getNameEdgeIdx(edgeIdx, Locale.ENGLISH);
9294
//TODO: decide between NonTransitMode and mode
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.conveyal.r5.rastercost;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedList;
5+
import java.util.List;
6+
import java.util.stream.Collectors;
7+
import java.util.stream.Stream;
8+
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import com.conveyal.r5.analyst.TravelTimeComputer;
13+
import com.conveyal.r5.profile.StreetPath;
14+
import com.conveyal.r5.streets.EdgeStore;
15+
import com.conveyal.r5.streets.PointSetTimes;
16+
import com.conveyal.r5.streets.StreetRouter;
17+
import com.conveyal.r5.transit.TransportNetwork;
18+
19+
/**
20+
* CustomCost helper for Greenpaths2 custom cost bi-objective routing
21+
* create a separate class for modularity and transparency
22+
* and distinguishing the custom cost logic from the rest of the base code
23+
*
24+
* Also this separate class can be helpful if we want to defreeze the R5 version
25+
*
26+
* Created by roope on 11.10.2023.
27+
*/
28+
29+
/* GP2 edit: create this class for handling custom cost logic */
30+
public class CustomCost {
31+
32+
private static final Logger LOG = LoggerFactory.getLogger(TravelTimeComputer.class);
33+
34+
/*
35+
* Get the osmIds from the router state
36+
* this is used in many-to-many or one-to-may matrices routing e.g. in TravelTimeComputer
37+
* these osmIds are needed for exposure based routing
38+
* they are combined by "osmid" with separately calculated exposure values
39+
* e.g. we calculate a dict of {osmid: exposure values}, run the matrix routing and then
40+
* see what osmid's we traversed and get the result by accessing the dict with the osmid as the key
41+
*
42+
*/
43+
public static List<List<Long>> getOsmIdsFromRouterState(PointSetTimes nonTransitTravelTimesToDestinations, StreetRouter sr, TransportNetwork network) {
44+
if(nonTransitTravelTimesToDestinations == null) return null;
45+
46+
// create a list of lists for osmIds
47+
// populate with empty lists
48+
// the first list will indicate the point index, the nested list has list of osmids created from StreetRouter.State and StreetPath
49+
List<List<Long>> osmIdResults = Stream.generate(ArrayList<Long>::new)
50+
.limit(nonTransitTravelTimesToDestinations.size())
51+
.collect(Collectors.toList());
52+
53+
// loop for each destination point in the matrix grid (i.e. the pointset)
54+
for(var i = 0; i < nonTransitTravelTimesToDestinations.size(); i++) {
55+
// get the lat and lon of the destination point
56+
double destPointLat = nonTransitTravelTimesToDestinations.pointSet.getLat(i);
57+
double destPointLon = nonTransitTravelTimesToDestinations.pointSet.getLon(i);
58+
59+
// get street router state using the current destination lat and lon
60+
StreetRouter.State lastState = sr.getState(destPointLat, destPointLon);
61+
if (lastState == null) {
62+
// skip the point if lat or lon is 0 or no state is found
63+
continue;
64+
}
65+
// create a street path using the state, used for looping all edges from the path
66+
StreetPath streetPath = new StreetPath(lastState, network, false);
67+
// get the all the edge indexes from the path
68+
LinkedList<Integer> pathEdges = streetPath.getEdges();
69+
// initialize empty list for osmIds
70+
LinkedList<Long> edgeOsmIdsForPath = new LinkedList<>();
71+
// loop through all the edges in the path traversed and get osmids from the edge store
72+
for (Integer edgeIdx : pathEdges) {
73+
EdgeStore.Edge edge = network.streetLayer.edgeStore.getCursor(edgeIdx);
74+
edgeOsmIdsForPath.add(edge.getOSMID());
75+
}
76+
// remove dublicate osmIds
77+
List<Long> uniqueEdgeOsmIdsForPath = edgeOsmIdsForPath.stream().distinct().collect(Collectors.toList());
78+
// replace the empty populated list with the unique list if any osmids are found
79+
if (!uniqueEdgeOsmIdsForPath.isEmpty()) {
80+
osmIdResults.set(i, uniqueEdgeOsmIdsForPath);
81+
}
82+
}
83+
// check if all lists are empty i.e. nothing was added
84+
// if all empty, return null
85+
if (osmIdResults.stream().allMatch(List::isEmpty)) {
86+
LOG.info("No OsmId's were found for any of the points");
87+
return null;
88+
}
89+
90+
return osmIdResults;
91+
}
92+
}

0 commit comments

Comments
 (0)