This commit is contained in:
Andras Schmelczer 2026-02-15 22:39:49 +00:00
parent 03445188ea
commit 524580eb25
102 changed files with 36625 additions and 1295 deletions

View file

@ -20,17 +20,25 @@ import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
/** R5 routing: network loading, point set construction, travel time computation. */
/** R5 routing: network loading, spatial filtering, travel time computation. */
public class Router {
private static final int ZOOM = 9;
private static final int ZOOM = 9; // R5 enforces range 9-12
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) {}
/** Result of computing travel times for a single origin with spatial pre-filtering. */
record FilteredResult(int[] originalIndices, short[] times) {}
/** Max plausible travel radius in km for 120-minute trips. */
static double maxRadiusKm(String mode) {
return switch (mode) {
case "car" -> 150;
case "transit" -> 150;
case "bicycle" -> 60;
case "walking" -> 12;
default -> throw new IllegalArgumentException("Unknown mode: " + mode);
};
}
/** Load or build the transport network with Kryo caching. */
static TransportNetwork loadNetwork(String dataDir, String cacheDir) throws Exception {
@ -56,10 +64,80 @@ public class Router {
}
/**
* 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.
* Filter destinations by distance, build chunks, compute travel times for one origin.
* Returns only the filtered subset indices and their travel times.
*/
static List<DestinationChunk> buildDestinationChunks(double[] lats, double[] lons) {
static FilteredResult computeForOrigin(
TransportNetwork network,
double[] allLats, double[] allLons,
double originLat, double originLon,
String mode, LocalDate date) {
double maxRadius = maxRadiusKm(mode);
// 1. Filter destinations by bounding box
int[] filtered = filterByDistance(allLats, allLons, originLat, originLon, maxRadius);
if (filtered.length == 0) {
return new FilteredResult(new int[0], new short[0]);
}
// 2. Extract filtered coordinate arrays
double[] fLats = new double[filtered.length];
double[] fLons = new double[filtered.length];
for (int i = 0; i < filtered.length; i++) {
fLats[i] = allLats[filtered[i]];
fLons[i] = allLons[filtered[i]];
}
// 3. Build chunks from filtered destinations
List<DestinationChunk> chunks = buildDestinationChunks(fLats, fLons);
// 4. Compute travel times
short[] times = computeTravelTimes(network, chunks, originLat, originLon, mode, fLats.length, date);
return new FilteredResult(filtered, times);
}
/**
* Filter destination indices to those within a bounding box of maxRadiusKm from origin.
* Uses degree-based approximation slightly overestimates at corners, which is fine.
*/
private static int[] filterByDistance(
double[] lats, double[] lons,
double originLat, double originLon,
double maxRadiusKm) {
double degLat = maxRadiusKm / 111.0;
double degLon = maxRadiusKm / (111.0 * Math.cos(Math.toRadians(originLat)));
double minLat = originLat - degLat;
double maxLat = originLat + degLat;
double minLon = originLon - degLon;
double maxLon = originLon + degLon;
// Two-pass: count then fill (avoids ArrayList/boxing overhead)
int count = 0;
for (int i = 0; i < lats.length; i++) {
if (lats[i] >= minLat && lats[i] <= maxLat && lons[i] >= minLon && lons[i] <= maxLon) {
count++;
}
}
int[] result = new int[count];
int j = 0;
for (int i = 0; i < lats.length; i++) {
if (lats[i] >= minLat && lats[i] <= maxLat && lons[i] >= minLon && lons[i] <= maxLon) {
result[j++] = i;
}
}
return result;
}
/**
* 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 is under 5M cells.
*/
private static List<DestinationChunk> buildDestinationChunks(double[] lats, double[] lons) {
int n = lats.length;
// Sort indices by latitude for geographic chunking
@ -94,13 +172,11 @@ public class Router {
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(
private static short[] computeTravelTimes(
TransportNetwork network, List<DestinationChunk> chunks,
double originLat, double originLon, String mode, int nDest, LocalDate date) {
@ -125,6 +201,10 @@ public class Router {
return times;
}
// --- Private helpers ---
private record DestinationChunk(FreeFormPointSet pointSet, WebMercatorExtents extents, int[] originalIndices) {}
private static DestinationChunk buildChunk(
double[] lats, double[] lons, Integer[] sorted, int start, int end) {
int size = end - start;