Can't even keep track anymore
This commit is contained in:
parent
dccc1e439d
commit
3a3f899ea2
50 changed files with 1144 additions and 560 deletions
|
|
@ -1,26 +1,11 @@
|
|||
FROM eclipse-temurin:21-jdk AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Download pre-built R5 fat JAR from GitHub Releases
|
||||
# Download pre-built R5 fat JAR from GitHub Releases (includes all R5 deps)
|
||||
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
|
||||
# Gson for JSON (HTTP server is built into JDK)
|
||||
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
|
||||
|
|
@ -30,4 +15,6 @@ 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"]
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
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()
|
||||
}
|
||||
18
r5-java/entrypoint.sh
Normal file
18
r5-java/entrypoint.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
TRANSIT_DIR=$DATA_DIR
|
||||
NETWORK_DIR=$NETWORK_CACHE_DIR
|
||||
BUILD_DIR="$NETWORK_DIR/build"
|
||||
|
||||
# If no cached network yet, copy transit data to a writable location for the build.
|
||||
# R5 writes temp files (.mapdb) next to the OSM/GTFS files during network construction.
|
||||
if [ ! -f "$NETWORK_DIR/network.dat" ]; then
|
||||
echo "No cached network — copying transit data to writable build dir..."
|
||||
mkdir -p "$BUILD_DIR"
|
||||
cp "$OSM_DIR"/*.osm.pbf "$BUILD_DIR/" 2>/dev/null || true
|
||||
cp "$TRANSIT_DIR"/*.zip "$BUILD_DIR/" 2>/dev/null || true
|
||||
export DATA_DIR="$BUILD_DIR"
|
||||
fi
|
||||
|
||||
exec java -Xmx16g -cp "out:lib/*" propertymap.App
|
||||
|
|
@ -1 +0,0 @@
|
|||
rootProject.name = 'r5-service'
|
||||
|
|
@ -1,16 +1,26 @@
|
|||
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 com.google.gson.Gson;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.Context;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.util.EnumSet;
|
||||
|
||||
|
|
@ -30,10 +40,17 @@ public class App {
|
|||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String dataDir = System.getenv("DATA_DIR");
|
||||
if (dataDir == null) dataDir = "/data/transit";
|
||||
|
||||
if (dataDir == null) {
|
||||
System.err.println("Error: DATA_DIR environment variable not set");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String networkCacheDir = System.getenv("NETWORK_CACHE_DIR");
|
||||
if (networkCacheDir == null) networkCacheDir = "/data/network";
|
||||
if (networkCacheDir == null) {
|
||||
System.err.println("Error: NETWORK_CACHE_DIR environment variable not set");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
System.out.println("Loading transport network from " + dataDir);
|
||||
System.out.println("Network cache dir: " + networkCacheDir);
|
||||
|
|
@ -41,51 +58,80 @@ public class App {
|
|||
File cacheFile = new File(networkCacheDir, "network.dat");
|
||||
if (cacheFile.exists()) {
|
||||
System.out.println("Loading cached network from " + cacheFile);
|
||||
network = TransportNetwork.read(cacheFile);
|
||||
network = KryoNetworkSerializer.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);
|
||||
KryoNetworkSerializer.write(network, cacheFile);
|
||||
System.out.println("Network cached to " + cacheFile);
|
||||
}
|
||||
|
||||
// Build stop-to-vertex distance tables (needed for egress routing in transit mode).
|
||||
// Not built by fromDirectory() and too large to fit in the Kryo cache with 4GB heap.
|
||||
System.out.println("Building stop-to-vertex distance tables...");
|
||||
network.transitLayer.buildDistanceTables(null);
|
||||
System.out.println("Distance tables built");
|
||||
|
||||
System.out.println("Transport network loaded successfully");
|
||||
|
||||
Javalin app = Javalin.create().start(8003);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(8003), 0);
|
||||
|
||||
app.get("/health", ctx -> ctx.result("ok"));
|
||||
server.createContext("/health", exchange -> {
|
||||
sendResponse(exchange, 200, "ok");
|
||||
});
|
||||
|
||||
app.post("/travel-times", App::handleTravelTimes);
|
||||
server.createContext("/travel-times", exchange -> {
|
||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
||||
sendResponse(exchange, 405, "Method not allowed");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
handleTravelTimes(exchange);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error handling travel-times: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
sendResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
server.setExecutor(java.util.concurrent.Executors.newFixedThreadPool(4));
|
||||
server.start();
|
||||
System.out.println("R5 service listening on port 8003");
|
||||
}
|
||||
|
||||
private static void handleTravelTimes(Context ctx) {
|
||||
private static void sendResponse(HttpExchange exchange, int status, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(status, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleTravelTimes(HttpExchange exchange) throws IOException {
|
||||
long t0 = System.currentTimeMillis();
|
||||
|
||||
TravelTimeRequest req = gson.fromJson(ctx.body(), TravelTimeRequest.class);
|
||||
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||
TravelTimeRequest req = gson.fromJson(body, TravelTimeRequest.class);
|
||||
|
||||
if (req.origin == null || req.origin.length != 2) {
|
||||
ctx.status(400).result("origin must be [lat, lon]");
|
||||
sendResponse(exchange, 400, "{\"error\":\"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]");
|
||||
sendResponse(exchange, 400, "{\"error\":\"destinations must be non-empty\"}");
|
||||
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];
|
||||
// Build destination point set (Coordinate takes x=lon, y=lat)
|
||||
Coordinate[] coords = new Coordinate[req.destinations.length];
|
||||
for (int i = 0; i < req.destinations.length; i++) {
|
||||
lats[i] = req.destinations[i][0];
|
||||
lons[i] = req.destinations[i][1];
|
||||
coords[i] = new Coordinate(req.destinations[i][1], req.destinations[i][0]); // lon, lat
|
||||
}
|
||||
|
||||
FreeFormPointSet destinations = new FreeFormPointSet(lats, lons);
|
||||
FreeFormPointSet destinations = new FreeFormPointSet(coords);
|
||||
|
||||
// Build the regional task
|
||||
RegionalTask task = new RegionalTask();
|
||||
|
|
@ -93,7 +139,16 @@ public class App {
|
|||
task.fromLon = req.origin[1];
|
||||
task.date = LocalDate.now();
|
||||
task.percentiles = new int[]{50};
|
||||
task.monteCarloDraws = 1;
|
||||
task.recordTimes = true;
|
||||
task.destinationPointSets = new PointSet[]{ destinations };
|
||||
|
||||
// Set grid extents from destination point set (required by TravelTimeComputer)
|
||||
WebMercatorExtents extents = destinations.getWebMercatorExtents();
|
||||
task.zoom = extents.zoom;
|
||||
task.west = extents.west;
|
||||
task.north = extents.north;
|
||||
task.width = extents.width;
|
||||
task.height = extents.height;
|
||||
|
||||
switch (mode) {
|
||||
case "car":
|
||||
|
|
@ -131,24 +186,31 @@ public class App {
|
|||
task.accessModes = EnumSet.of(LegMode.WALK);
|
||||
task.egressModes = EnumSet.of(LegMode.WALK);
|
||||
task.directModes = EnumSet.of(LegMode.WALK);
|
||||
task.transitModes = EnumSet.allOf(TransitModes.class);
|
||||
task.transitModes = EnumSet.of(TransitModes.TRANSIT);
|
||||
break;
|
||||
}
|
||||
|
||||
// Compute travel times
|
||||
TravelTimeComputer computer = new TravelTimeComputer(task, network, destinations);
|
||||
int[][] results = computer.computeTravelTimes();
|
||||
TravelTimeComputer computer = new TravelTimeComputer(task, network);
|
||||
OneOriginResult result = 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
|
||||
TravelTimeResult tt = result.travelTimes;
|
||||
if (tt != null) {
|
||||
int[][] values = tt.getValues();
|
||||
// values[percentileIndex][destinationIndex]
|
||||
for (int i = 0; i < req.destinations.length; i++) {
|
||||
if (i < values[0].length && values[0][i] != Integer.MAX_VALUE) {
|
||||
response.travel_times[i] = values[0][i]; // already in minutes
|
||||
} else {
|
||||
response.travel_times[i] = -1; // unreachable
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < req.destinations.length; i++) {
|
||||
response.travel_times[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +218,6 @@ public class App {
|
|||
System.out.println("Travel times (" + mode + ") computed for " + req.destinations.length +
|
||||
" destinations in " + elapsed + "ms");
|
||||
|
||||
ctx.json(response);
|
||||
sendResponse(exchange, 200, gson.toJson(response));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue