Migrate to java

This commit is contained in:
Andras Schmelczer 2026-02-11 21:33:14 +00:00
parent 1588c01b19
commit dccc1e439d
8 changed files with 243 additions and 192 deletions

33
r5-java/Dockerfile Normal file
View file

@ -0,0 +1,33 @@
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
# Download pre-built R5 fat JAR from GitHub Releases
ADD https://github.com/conveyal/r5/releases/download/v7.5/r5-v7.5-all.jar /app/lib/r5.jar
# Download Javalin + Gson + SLF4J
ADD https://repo1.maven.org/maven2/io/javalin/javalin/6.4.0/javalin-6.4.0.jar /app/lib/javalin.jar
ADD https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar /app/lib/gson.jar
ADD https://repo1.maven.org/maven2/org/slf4j/slf4j-simple/2.0.16/slf4j-simple-2.0.16.jar /app/lib/slf4j-simple.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-server/11.0.24/jetty-server-11.0.24.jar /app/lib/jetty-server.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-util/11.0.24/jetty-util-11.0.24.jar /app/lib/jetty-util.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-http/11.0.24/jetty-http-11.0.24.jar /app/lib/jetty-http.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-io/11.0.24/jetty-io-11.0.24.jar /app/lib/jetty-io.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-servlet/11.0.24/jetty-servlet-11.0.24.jar /app/lib/jetty-servlet.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-security/11.0.24/jetty-security-11.0.24.jar /app/lib/jetty-security.jar
ADD https://repo1.maven.org/maven2/jakarta/servlet/jakarta.servlet-api/5.0.0/jakarta.servlet-api-5.0.0.jar /app/lib/servlet-api.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/websocket/websocket-jetty-server/11.0.24/websocket-jetty-server-11.0.24.jar /app/lib/ws-server.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/websocket/websocket-jetty-api/11.0.24/websocket-jetty-api-11.0.24.jar /app/lib/ws-api.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/websocket/websocket-core-server/11.0.24/websocket-core-server-11.0.24.jar /app/lib/ws-core-server.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/websocket/websocket-core-common/11.0.24/websocket-core-common-11.0.24.jar /app/lib/ws-core-common.jar
ADD https://repo1.maven.org/maven2/org/eclipse/jetty/websocket/websocket-servlet/11.0.24/websocket-servlet-11.0.24.jar /app/lib/ws-servlet.jar
ADD https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.1.0/kotlin-stdlib-2.1.0.jar /app/lib/kotlin-stdlib.jar
COPY src/ src/
RUN javac -cp "lib/*" -d out src/main/java/propertymap/App.java
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/lib/ /app/lib/
COPY --from=build /app/out/ /app/out/
ENTRYPOINT ["java", "-Xmx4g", "-cp", "out:lib/*", "propertymap.App"]

37
r5-java/build.gradle Normal file
View file

@ -0,0 +1,37 @@
plugins {
id 'java'
id 'application'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
mainClass = 'propertymap.App'
}
dependencies {
implementation 'com.conveyal:r5:7.2'
implementation 'io.javalin:javalin:6.4.0'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'org.slf4j:slf4j-simple:2.0.16'
}
jar {
manifest {
attributes 'Main-Class': 'propertymap.App'
}
}
shadowJar {
archiveClassifier = ''
mergeServiceFiles()
}

1
r5-java/settings.gradle Normal file
View file

@ -0,0 +1 @@
rootProject.name = 'r5-service'

View file

@ -0,0 +1,161 @@
package propertymap;
import com.conveyal.r5.analyst.FreeFormPointSet;
import com.conveyal.r5.analyst.TravelTimeComputer;
import com.conveyal.r5.analyst.cluster.RegionalTask;
import com.conveyal.r5.api.util.LegMode;
import com.conveyal.r5.api.util.TransitModes;
import com.conveyal.r5.transit.TransportNetwork;
import com.google.gson.Gson;
import io.javalin.Javalin;
import io.javalin.http.Context;
import java.io.File;
import java.time.LocalDate;
import java.util.EnumSet;
public class App {
private static TransportNetwork network;
private static final Gson gson = new Gson();
static class TravelTimeRequest {
double[] origin; // [lat, lon]
double[][] destinations; // [[lat, lon], ...]
String mode; // "transit", "car", "bicycle", "walking"
}
static class TravelTimeResponse {
double[] travel_times; // minutes, -1 = unreachable
}
public static void main(String[] args) throws Exception {
String dataDir = System.getenv("DATA_DIR");
if (dataDir == null) dataDir = "/data/transit";
String networkCacheDir = System.getenv("NETWORK_CACHE_DIR");
if (networkCacheDir == null) networkCacheDir = "/data/network";
System.out.println("Loading transport network from " + dataDir);
System.out.println("Network cache dir: " + networkCacheDir);
File cacheFile = new File(networkCacheDir, "network.dat");
if (cacheFile.exists()) {
System.out.println("Loading cached network from " + cacheFile);
network = TransportNetwork.read(cacheFile);
} else {
System.out.println("Building network (first run, this takes a few minutes)...");
network = TransportNetwork.fromDirectory(new File(dataDir));
new File(networkCacheDir).mkdirs();
network.write(cacheFile);
System.out.println("Network cached to " + cacheFile);
}
System.out.println("Transport network loaded successfully");
Javalin app = Javalin.create().start(8003);
app.get("/health", ctx -> ctx.result("ok"));
app.post("/travel-times", App::handleTravelTimes);
System.out.println("R5 service listening on port 8003");
}
private static void handleTravelTimes(Context ctx) {
long t0 = System.currentTimeMillis();
TravelTimeRequest req = gson.fromJson(ctx.body(), TravelTimeRequest.class);
if (req.origin == null || req.origin.length != 2) {
ctx.status(400).result("origin must be [lat, lon]");
return;
}
if (req.destinations == null || req.destinations.length == 0) {
ctx.status(400).result("destinations must be non-empty array of [lat, lon]");
return;
}
String mode = req.mode != null ? req.mode : "transit";
// Build destination point set
double[] lats = new double[req.destinations.length];
double[] lons = new double[req.destinations.length];
for (int i = 0; i < req.destinations.length; i++) {
lats[i] = req.destinations[i][0];
lons[i] = req.destinations[i][1];
}
FreeFormPointSet destinations = new FreeFormPointSet(lats, lons);
// Build the regional task
RegionalTask task = new RegionalTask();
task.fromLat = req.origin[0];
task.fromLon = req.origin[1];
task.date = LocalDate.now();
task.percentiles = new int[]{50};
task.monteCarloDraws = 1;
switch (mode) {
case "car":
task.fromTime = 8 * 3600;
task.toTime = 8 * 3600 + 60;
task.maxTripDurationMinutes = 120;
task.accessModes = EnumSet.of(LegMode.CAR);
task.egressModes = EnumSet.of(LegMode.CAR);
task.directModes = EnumSet.of(LegMode.CAR);
task.transitModes = EnumSet.noneOf(TransitModes.class);
break;
case "bicycle":
task.fromTime = 8 * 3600;
task.toTime = 8 * 3600 + 60;
task.maxTripDurationMinutes = 90;
task.accessModes = EnumSet.of(LegMode.BICYCLE);
task.egressModes = EnumSet.of(LegMode.BICYCLE);
task.directModes = EnumSet.of(LegMode.BICYCLE);
task.transitModes = EnumSet.noneOf(TransitModes.class);
break;
case "walking":
task.fromTime = 8 * 3600;
task.toTime = 8 * 3600 + 60;
task.maxTripDurationMinutes = 60;
task.accessModes = EnumSet.of(LegMode.WALK);
task.egressModes = EnumSet.of(LegMode.WALK);
task.directModes = EnumSet.of(LegMode.WALK);
task.transitModes = EnumSet.noneOf(TransitModes.class);
break;
default: // transit
task.fromTime = 8 * 3600;
task.toTime = 8 * 3600 + 60; // single RAPTOR sweep
task.maxTripDurationMinutes = 90;
task.maxRides = 4;
task.accessModes = EnumSet.of(LegMode.WALK);
task.egressModes = EnumSet.of(LegMode.WALK);
task.directModes = EnumSet.of(LegMode.WALK);
task.transitModes = EnumSet.allOf(TransitModes.class);
break;
}
// Compute travel times
TravelTimeComputer computer = new TravelTimeComputer(task, network, destinations);
int[][] results = computer.computeTravelTimes();
// results[percentileIdx][destinationIdx] we only have 1 percentile (index 0)
TravelTimeResponse response = new TravelTimeResponse();
response.travel_times = new double[req.destinations.length];
int[] times = results[0]; // percentile 0 (the 50th percentile)
for (int i = 0; i < req.destinations.length; i++) {
if (i < times.length && times[i] != Integer.MAX_VALUE) {
response.travel_times[i] = times[i]; // already in minutes
} else {
response.travel_times[i] = -1; // unreachable
}
}
long elapsed = System.currentTimeMillis() - t0;
System.out.println("Travel times (" + mode + ") computed for " + req.destinations.length +
" destinations in " + elapsed + "ms");
ctx.json(response);
}
}