Migrate to java
This commit is contained in:
parent
1588c01b19
commit
dccc1e439d
8 changed files with 243 additions and 192 deletions
|
|
@ -21,6 +21,8 @@ services:
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
POCKETBASE_URL: http://pocketbase:8090
|
POCKETBASE_URL: http://pocketbase:8090
|
||||||
|
POCKETBASE_ADMIN_EMAIL: ${POCKETBASE_ADMIN_EMAIL:-}
|
||||||
|
POCKETBASE_ADMIN_PASSWORD: ${POCKETBASE_ADMIN_PASSWORD:-}
|
||||||
SCREENSHOT_URL: http://screenshot:8002
|
SCREENSHOT_URL: http://screenshot:8002
|
||||||
OLLAMA_URL: http://host.docker.internal:11434
|
OLLAMA_URL: http://host.docker.internal:11434
|
||||||
R5_URL: http://r5:8003
|
R5_URL: http://r5:8003
|
||||||
|
|
@ -86,23 +88,23 @@ services:
|
||||||
start_period: 5s
|
start_period: 5s
|
||||||
|
|
||||||
r5:
|
r5:
|
||||||
build: ./r5-service
|
build: ./r5-java
|
||||||
ports:
|
ports:
|
||||||
- "8003:8003"
|
- "8004:8003"
|
||||||
networks:
|
networks:
|
||||||
- dev-network
|
- dev-network
|
||||||
volumes:
|
volumes:
|
||||||
- ./property-data/transit:/data/transit
|
- r5-network:/data/network
|
||||||
- r5-cache:/root/.cache/r5py
|
- ./property-data/transit:/data/transit:ro
|
||||||
environment:
|
environment:
|
||||||
TRANSIT_DATA_DIR: /data/transit
|
DATA_DIR: /data/transit
|
||||||
|
NETWORK_CACHE_DIR: /data/network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8003/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8003/health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 5
|
||||||
start_period: 600s
|
start_period: 300s
|
||||||
init: true
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pb-data:
|
pb-data:
|
||||||
|
|
@ -110,7 +112,7 @@ volumes:
|
||||||
cargo-target:
|
cargo-target:
|
||||||
frontend-node-modules:
|
frontend-node-modules:
|
||||||
screenshot-cache:
|
screenshot-cache:
|
||||||
r5-cache:
|
r5-network:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dev-network:
|
dev-network:
|
||||||
|
|
|
||||||
33
r5-java/Dockerfile
Normal file
33
r5-java/Dockerfile
Normal 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
37
r5-java/build.gradle
Normal 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
1
r5-java/settings.gradle
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = 'r5-service'
|
||||||
161
r5-java/src/main/java/propertymap/App.java
Normal file
161
r5-java/src/main/java/propertymap/App.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
||||||
|
|
||||||
# r5py needs a JVM to run the R5 routing engine
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends openjdk-21-jre-headless curl libexpat1 libgdal-dev && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY pyproject.toml .
|
|
||||||
RUN uv pip install --system --no-cache -r pyproject.toml
|
|
||||||
|
|
||||||
COPY main.py .
|
|
||||||
|
|
||||||
EXPOSE 8003
|
|
||||||
|
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8003"]
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
"""R5 travel time service — FastAPI wrapper around r5py.
|
|
||||||
|
|
||||||
Loads an OSM PBF + GTFS feeds at startup, then serves many-to-one
|
|
||||||
travel time queries via POST /travel-times.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import geopandas as gpd
|
|
||||||
import pandas as pd
|
|
||||||
import r5py
|
|
||||||
from fastapi import FastAPI, HTTPException
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from shapely.geometry import Point
|
|
||||||
|
|
||||||
logger = logging.getLogger("r5-service")
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
app = FastAPI(title="R5 Travel Time Service")
|
|
||||||
|
|
||||||
# Global transport network — loaded once at startup
|
|
||||||
_transport_network: r5py.TransportNetwork | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
def load_network() -> None:
|
|
||||||
global _transport_network
|
|
||||||
|
|
||||||
data_dir = Path(os.environ.get("TRANSIT_DATA_DIR", "/data/transit"))
|
|
||||||
|
|
||||||
osm_files = list(data_dir.glob("*.osm.pbf"))
|
|
||||||
if not osm_files:
|
|
||||||
raise RuntimeError(f"No .osm.pbf file found in {data_dir}")
|
|
||||||
osm_pbf = osm_files[0]
|
|
||||||
|
|
||||||
gtfs_files = list(data_dir.glob("*.zip"))
|
|
||||||
logger.info(
|
|
||||||
"Loading transport network: OSM=%s, GTFS=%s",
|
|
||||||
osm_pbf.name,
|
|
||||||
[f.name for f in gtfs_files],
|
|
||||||
)
|
|
||||||
|
|
||||||
_transport_network = r5py.TransportNetwork(
|
|
||||||
osm_pbf=osm_pbf,
|
|
||||||
gtfs=gtfs_files if gtfs_files else None,
|
|
||||||
)
|
|
||||||
logger.info("Transport network loaded successfully")
|
|
||||||
|
|
||||||
|
|
||||||
# ── Request / Response models ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# r5py 1.x uses transport_modes for direct modes and access_modes for
|
|
||||||
# first-mile walking/cycling to transit.
|
|
||||||
MODE_CONFIGS = {
|
|
||||||
"transit": {
|
|
||||||
"transport_modes": [r5py.TransportMode.TRANSIT],
|
|
||||||
"access_modes": [r5py.TransportMode.WALK],
|
|
||||||
},
|
|
||||||
"car": {
|
|
||||||
"transport_modes": [r5py.TransportMode.CAR],
|
|
||||||
},
|
|
||||||
"bicycle": {
|
|
||||||
"transport_modes": [r5py.TransportMode.BICYCLE],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TravelTimeRequest(BaseModel):
|
|
||||||
origins: list[list[float]] # [[lat, lon], ...]
|
|
||||||
destination: list[float] # [lat, lon]
|
|
||||||
mode: str = "transit"
|
|
||||||
departure_time: str | None = None # ISO 8601, defaults to next weekday 8am
|
|
||||||
|
|
||||||
|
|
||||||
class TravelTimeResponse(BaseModel):
|
|
||||||
travel_times: list[float | None] # minutes per origin, null if unreachable
|
|
||||||
|
|
||||||
|
|
||||||
# ── Endpoints ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
def health() -> dict:
|
|
||||||
if _transport_network is None:
|
|
||||||
raise HTTPException(status_code=503, detail="Network not loaded")
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/travel-times", response_model=TravelTimeResponse)
|
|
||||||
def compute_travel_times(req: TravelTimeRequest) -> TravelTimeResponse:
|
|
||||||
if _transport_network is None:
|
|
||||||
raise HTTPException(status_code=503, detail="Network not loaded")
|
|
||||||
|
|
||||||
if req.mode not in MODE_CONFIGS:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=f"Invalid mode '{req.mode}'. Must be one of: {list(MODE_CONFIGS)}",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not req.origins:
|
|
||||||
return TravelTimeResponse(travel_times=[])
|
|
||||||
|
|
||||||
# Parse departure time
|
|
||||||
if req.departure_time:
|
|
||||||
departure = datetime.datetime.fromisoformat(req.departure_time)
|
|
||||||
else:
|
|
||||||
# Default: next weekday at 8:00 AM
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
departure = now.replace(hour=8, minute=0, second=0, microsecond=0)
|
|
||||||
# Advance to next weekday if weekend
|
|
||||||
while departure.weekday() >= 5:
|
|
||||||
departure += datetime.timedelta(days=1)
|
|
||||||
|
|
||||||
# Build origin GeoDataFrame (note: Point takes (lon, lat))
|
|
||||||
origin_points = [Point(lon, lat) for lat, lon in req.origins]
|
|
||||||
origins_gdf = gpd.GeoDataFrame(
|
|
||||||
{"id": range(len(origin_points))},
|
|
||||||
geometry=origin_points,
|
|
||||||
crs="EPSG:4326",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build destination GeoDataFrame
|
|
||||||
dest_lat, dest_lon = req.destination
|
|
||||||
dest_gdf = gpd.GeoDataFrame(
|
|
||||||
{"id": [0]},
|
|
||||||
geometry=[Point(dest_lon, dest_lat)],
|
|
||||||
crs="EPSG:4326",
|
|
||||||
)
|
|
||||||
|
|
||||||
mode_config = MODE_CONFIGS[req.mode]
|
|
||||||
|
|
||||||
# r5py 1.x: TravelTimeMatrix is instantiated directly and IS the result
|
|
||||||
result = r5py.TravelTimeMatrix(
|
|
||||||
_transport_network,
|
|
||||||
origins=origins_gdf,
|
|
||||||
destinations=dest_gdf,
|
|
||||||
departure=departure,
|
|
||||||
**mode_config,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build response: one travel time per origin
|
|
||||||
# r5py 1.x returns a GeoDataFrame with columns: from_id, to_id, travel_time
|
|
||||||
travel_times: list[float | None] = [None] * len(req.origins)
|
|
||||||
for _, row in result.iterrows():
|
|
||||||
origin_idx = int(row["from_id"])
|
|
||||||
tt = row["travel_time"]
|
|
||||||
if pd.notna(tt) and tt >= 0:
|
|
||||||
travel_times[origin_idx] = float(tt)
|
|
||||||
|
|
||||||
return TravelTimeResponse(travel_times=travel_times)
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "r5-service"
|
|
||||||
version = "0.1.0"
|
|
||||||
requires-python = ">=3.12"
|
|
||||||
dependencies = [
|
|
||||||
"r5py>=0.8",
|
|
||||||
"fastapi>=0.115",
|
|
||||||
"uvicorn>=0.34",
|
|
||||||
"geopandas>=1.0",
|
|
||||||
"shapely>=2.0",
|
|
||||||
]
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue