More
This commit is contained in:
parent
128b3191e7
commit
03445188ea
54 changed files with 596953 additions and 3577 deletions
211
r5-java/src/main/java/propertymap/Router.java
Normal file
211
r5-java/src/main/java/propertymap/Router.java
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
package propertymap;
|
||||
|
||||
import com.conveyal.r5.OneOriginResult;
|
||||
import com.conveyal.r5.analyst.FreeFormPointSet;
|
||||
import com.conveyal.r5.analyst.PointSet;
|
||||
import com.conveyal.r5.analyst.TravelTimeComputer;
|
||||
import com.conveyal.r5.analyst.WebMercatorExtents;
|
||||
import com.conveyal.r5.analyst.cluster.RegionalTask;
|
||||
import com.conveyal.r5.analyst.cluster.TravelTimeResult;
|
||||
import com.conveyal.r5.api.util.LegMode;
|
||||
import com.conveyal.r5.api.util.TransitModes;
|
||||
import com.conveyal.r5.kryo.KryoNetworkSerializer;
|
||||
import com.conveyal.r5.transit.TransportNetwork;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
/** R5 routing: network loading, point set construction, travel time computation. */
|
||||
public class Router {
|
||||
|
||||
private static final int ZOOM = 9;
|
||||
private static final int MAX_GRID_CELLS = 4_900_000; // under R5's 5M limit
|
||||
|
||||
/**
|
||||
* A chunk of destinations that fits within R5's grid cell limit at zoom 9.
|
||||
* originalIndices maps each position in this chunk back to the full destinations array.
|
||||
*/
|
||||
record DestinationChunk(FreeFormPointSet pointSet, WebMercatorExtents extents, int[] originalIndices) {}
|
||||
|
||||
/** Load or build the transport network with Kryo caching. */
|
||||
static TransportNetwork loadNetwork(String dataDir, String cacheDir) throws Exception {
|
||||
System.err.println("Loading transport network...");
|
||||
File cacheFile = new File(cacheDir, "network.dat");
|
||||
TransportNetwork network;
|
||||
|
||||
if (cacheFile.exists()) {
|
||||
System.err.println(" Loading cached network from " + cacheFile);
|
||||
network = KryoNetworkSerializer.read(cacheFile);
|
||||
} else {
|
||||
System.err.println(" Building network (first run, takes a few minutes)...");
|
||||
network = TransportNetwork.fromDirectory(new File(dataDir));
|
||||
new File(cacheDir).mkdirs();
|
||||
KryoNetworkSerializer.write(network, cacheFile);
|
||||
System.err.println(" Cached to " + cacheFile);
|
||||
}
|
||||
|
||||
System.err.println(" Building distance tables...");
|
||||
network.transitLayer.buildDistanceTables(null);
|
||||
System.err.println(" Network ready");
|
||||
return network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split destinations into geographic chunks that each fit within R5's grid cell limit.
|
||||
* Sorts by latitude and splits into bands so each band's bounding box at zoom 9 is under 5M cells.
|
||||
*/
|
||||
static List<DestinationChunk> buildDestinationChunks(double[] lats, double[] lons) {
|
||||
int n = lats.length;
|
||||
|
||||
// Sort indices by latitude for geographic chunking
|
||||
Integer[] sorted = new Integer[n];
|
||||
for (int i = 0; i < n; i++) sorted[i] = i;
|
||||
Arrays.sort(sorted, (a, b) -> Double.compare(lats[a], lats[b]));
|
||||
|
||||
// Determine grid width (longitude span is the same for all chunks)
|
||||
double minLon = Double.MAX_VALUE, maxLon = -Double.MAX_VALUE;
|
||||
for (double lon : lons) {
|
||||
minLon = Math.min(minLon, lon);
|
||||
maxLon = Math.max(maxLon, lon);
|
||||
}
|
||||
int totalPixels = 256 << ZOOM;
|
||||
int gridWidth = lonToPixel(maxLon, totalPixels) - lonToPixel(minLon, totalPixels) + 1;
|
||||
int maxHeight = MAX_GRID_CELLS / gridWidth;
|
||||
|
||||
// Greedily build chunks: extend each band until it would exceed maxHeight
|
||||
List<DestinationChunk> chunks = new ArrayList<>();
|
||||
int start = 0;
|
||||
while (start < n) {
|
||||
int end = start + 1;
|
||||
int topPixel = latToPixel(lats[sorted[start]], totalPixels);
|
||||
|
||||
while (end < n) {
|
||||
int bottomPixel = latToPixel(lats[sorted[end]], totalPixels);
|
||||
if (Math.abs(bottomPixel - topPixel) + 1 > maxHeight) break;
|
||||
end++;
|
||||
}
|
||||
|
||||
chunks.add(buildChunk(lats, lons, sorted, start, end));
|
||||
start = end;
|
||||
}
|
||||
|
||||
System.err.printf(" Split into %d chunks at zoom %d (grid width %d, max height %d)%n",
|
||||
chunks.size(), ZOOM, gridWidth, maxHeight);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/** Compute travel times from one origin to all destinations across all chunks. */
|
||||
static short[] computeTravelTimes(
|
||||
TransportNetwork network, List<DestinationChunk> chunks,
|
||||
double originLat, double originLon, String mode, int nDest, LocalDate date) {
|
||||
|
||||
short[] times = new short[nDest];
|
||||
Arrays.fill(times, (short) -1);
|
||||
|
||||
for (DestinationChunk chunk : chunks) {
|
||||
RegionalTask task = buildTask(chunk, originLat, originLon, mode, date);
|
||||
TravelTimeComputer computer = new TravelTimeComputer(task, network);
|
||||
OneOriginResult result = computer.computeTravelTimes();
|
||||
|
||||
TravelTimeResult tt = result.travelTimes;
|
||||
if (tt != null) {
|
||||
int[][] values = tt.getValues();
|
||||
for (int i = 0; i < chunk.originalIndices.length && i < values[0].length; i++) {
|
||||
if (values[0][i] != Integer.MAX_VALUE) {
|
||||
times[chunk.originalIndices[i]] = (short) values[0][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return times;
|
||||
}
|
||||
|
||||
private static DestinationChunk buildChunk(
|
||||
double[] lats, double[] lons, Integer[] sorted, int start, int end) {
|
||||
int size = end - start;
|
||||
int[] originalIndices = new int[size];
|
||||
Coordinate[] coords = new Coordinate[size];
|
||||
double minLat = Double.MAX_VALUE, maxLat = -Double.MAX_VALUE;
|
||||
double minLon = Double.MAX_VALUE, maxLon = -Double.MAX_VALUE;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
int idx = sorted[start + i];
|
||||
originalIndices[i] = idx;
|
||||
double lat = lats[idx], lon = lons[idx];
|
||||
coords[i] = new Coordinate(lon, lat); // x=lon, y=lat
|
||||
minLat = Math.min(minLat, lat);
|
||||
maxLat = Math.max(maxLat, lat);
|
||||
minLon = Math.min(minLon, lon);
|
||||
maxLon = Math.max(maxLon, lon);
|
||||
}
|
||||
|
||||
FreeFormPointSet pointSet = new FreeFormPointSet(coords);
|
||||
int totalPixels = 256 << ZOOM;
|
||||
int west = lonToPixel(minLon, totalPixels);
|
||||
int north = latToPixel(maxLat, totalPixels);
|
||||
int width = lonToPixel(maxLon, totalPixels) - west + 1;
|
||||
int height = latToPixel(minLat, totalPixels) - north + 1;
|
||||
WebMercatorExtents extents = new WebMercatorExtents(west, north, width, height, ZOOM);
|
||||
|
||||
return new DestinationChunk(pointSet, extents, originalIndices);
|
||||
}
|
||||
|
||||
private static RegionalTask buildTask(
|
||||
DestinationChunk chunk, double originLat, double originLon, String mode, LocalDate date) {
|
||||
RegionalTask task = new RegionalTask();
|
||||
task.fromLat = originLat;
|
||||
task.fromLon = originLon;
|
||||
task.date = date;
|
||||
task.percentiles = new int[]{50};
|
||||
task.recordTimes = true;
|
||||
task.destinationPointSets = new PointSet[]{chunk.pointSet};
|
||||
task.zoom = chunk.extents.zoom;
|
||||
task.west = chunk.extents.west;
|
||||
task.north = chunk.extents.north;
|
||||
task.width = chunk.extents.width;
|
||||
task.height = chunk.extents.height;
|
||||
task.fromTime = 8 * 3600;
|
||||
task.toTime = 8 * 3600 + 60;
|
||||
task.maxTripDurationMinutes = 120;
|
||||
|
||||
configureMode(task, mode);
|
||||
return task;
|
||||
}
|
||||
|
||||
private static void configureMode(RegionalTask task, String mode) {
|
||||
switch (mode) {
|
||||
case "car" -> setDirectMode(task, LegMode.CAR);
|
||||
case "bicycle" -> setDirectMode(task, LegMode.BICYCLE);
|
||||
case "walking" -> setDirectMode(task, LegMode.WALK);
|
||||
case "transit" -> {
|
||||
task.maxRides = 4;
|
||||
task.accessModes = EnumSet.of(LegMode.WALK);
|
||||
task.egressModes = EnumSet.of(LegMode.WALK);
|
||||
task.directModes = EnumSet.of(LegMode.WALK);
|
||||
task.transitModes = EnumSet.of(TransitModes.TRANSIT);
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unknown mode: " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setDirectMode(RegionalTask task, LegMode legMode) {
|
||||
task.accessModes = EnumSet.of(legMode);
|
||||
task.egressModes = EnumSet.of(legMode);
|
||||
task.directModes = EnumSet.of(legMode);
|
||||
task.transitModes = EnumSet.noneOf(TransitModes.class);
|
||||
}
|
||||
|
||||
private static int lonToPixel(double lon, int totalPixels) {
|
||||
return (int) Math.floor(totalPixels * (lon + 180.0) / 360.0);
|
||||
}
|
||||
|
||||
private static int latToPixel(double lat, int totalPixels) {
|
||||
double latRad = Math.toRadians(lat);
|
||||
return (int) Math.floor(totalPixels * (1.0 - Math.log(Math.tan(latRad) + 1.0 / Math.cos(latRad)) / Math.PI) / 2.0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue