Merge pull request #341 from openbikesensor/next-semaphore-with-road-usage

Add export of road usage after review (without temporal filtering)
This commit is contained in:
gluap 2023-05-29 13:22:16 +02:00 committed by GitHub
commit b1cfd30da9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 13 deletions

View file

@ -21,7 +21,7 @@ RUN npm run build
# Build the API and add the built frontend to it # Build the API and add the built frontend to it
############################################# #############################################
FROM python:3.9.7-bullseye FROM python:3.11.3-bullseye
RUN apt-get update &&\ RUN apt-get update &&\
apt-get install -y \ apt-get install -y \

View file

@ -1,4 +1,4 @@
FROM python:3.9.7-bullseye FROM python:3.11.3-bullseye
WORKDIR /opt/obs/api WORKDIR /opt/obs/api

View file

@ -3,11 +3,12 @@ from enum import Enum
from contextlib import contextmanager from contextlib import contextmanager
import zipfile import zipfile
import io import io
import re
from sqlite3 import connect from sqlite3 import connect
import shapefile import shapefile
from obs.api.db import OvertakingEvent from obs.api.db import OvertakingEvent
from sqlalchemy import select, func from sqlalchemy import select, func, text
from sanic.response import raw from sanic.response import raw
from sanic.exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
@ -39,11 +40,11 @@ PROJECTION_4326 = (
@contextmanager @contextmanager
def shapefile_zip(): def shapefile_zip(shape_type=shapefile.POINT, basename="events"):
zip_buffer = io.BytesIO() zip_buffer = io.BytesIO()
shp, shx, dbf = (io.BytesIO() for _ in range(3)) shp, shx, dbf = (io.BytesIO() for _ in range(3))
writer = shapefile.Writer( writer = shapefile.Writer(
shp=shp, shx=shx, dbf=dbf, shapeType=shapefile.POINT, encoding="utf8" shp=shp, shx=shx, dbf=dbf, shapeType=shape_type, encoding="utf8"
) )
yield writer, zip_buffer yield writer, zip_buffer
@ -52,10 +53,10 @@ def shapefile_zip():
writer.close() writer.close()
zip_file = zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) zip_file = zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False)
zip_file.writestr("events.shp", shp.getbuffer()) zip_file.writestr(f"{basename}.shp", shp.getbuffer())
zip_file.writestr("events.shx", shx.getbuffer()) zip_file.writestr(f"{basename}.shx", shx.getbuffer())
zip_file.writestr("events.dbf", dbf.getbuffer()) zip_file.writestr(f"{basename}.dbf", dbf.getbuffer())
zip_file.writestr("events.prj", PROJECTION_4326) zip_file.writestr(f"{basename}.prj", PROJECTION_4326)
zip_file.close() zip_file.close()
@ -74,7 +75,7 @@ async def export_events(req):
) )
if fmt == ExportFormat.SHAPEFILE: if fmt == ExportFormat.SHAPEFILE:
with shapefile_zip() as (writer, zip_buffer): with shapefile_zip(basename="events") as (writer, zip_buffer):
writer.field("distance_overtaker", "N", decimal=4) writer.field("distance_overtaker", "N", decimal=4)
writer.field("distance_stationary", "N", decimal=4) writer.field("distance_stationary", "N", decimal=4)
writer.field("way_id", "N", decimal=0) writer.field("way_id", "N", decimal=0)
@ -119,3 +120,73 @@ async def export_events(req):
return json_response(geojson) return json_response(geojson)
raise InvalidUsage("unknown export format") raise InvalidUsage("unknown export format")
@api.get(r"/export/segments")
async def export_segments(req):
async with use_request_semaphore(req, "export_semaphore", timeout=30):
bbox = req.ctx.get_single_arg(
"bbox", default="-180,-90,180,90"
)
assert re.match(r"(-?\d+\.?\d+,?){4}", bbox)
fmt = req.ctx.get_single_arg("fmt", convert=ExportFormat)
segments = await req.ctx.db.stream(
text(
f"select ST_AsGeoJSON(ST_Transform(geometry,4326)) AS geometry, way_id, distance_overtaker_mean, distance_overtaker_min,distance_overtaker_max,distance_overtaker_median,overtaking_event_count,usage_count,direction,zone,offset_direction,distance_overtaker_array from layer_obs_roads(ST_Transform(ST_MakeEnvelope({bbox},4326),3857),11,NULL,'1900-01-01'::timestamp,'2100-01-01'::timestamp) WHERE usage_count>0"
)
)
if fmt == ExportFormat.SHAPEFILE:
with shapefile_zip(shape_type=3, basename="segments") as (writer, zip_buffer):
writer.field("distance_overtaker_mean", "N", decimal=4)
writer.field("distance_overtaker_max", "N", decimal=4)
writer.field("distance_overtaker_min", "N", decimal=4)
writer.field("distance_overtaker_median", "N", decimal=4)
writer.field("overtaking_event_count", "N", decimal=4)
writer.field("usage_count", "N", decimal=4)
writer.field("way_id", "N", decimal=0)
writer.field("direction", "N", decimal=0)
writer.field("zone", "C")
async for segment in segments:
geom = json.loads(segment.st_asgeojson)
writer.line([geom["coordinates"]])
writer.record(
distance_overtaker_mean=segment.distance_overtaker_mean,
distance_overtaker_median=segment.distance_overtaker_median,
distance_overtaker_max=segment.distance_overtaker_max,
distance_overtaker_min=segment.distance_overtaker_min,
usage_count=segment.usage_count,
overtaking_event_count=segment.overtaking_event_count,
direction=segment.direction,
way_id=segment.way_id,
zone=segment.zone,
)
return raw(zip_buffer.getbuffer())
if fmt == ExportFormat.GEOJSON:
features = []
async for segment in segments:
features.append(
{
"type": "Feature",
"geometry": json.loads(segment.geometry),
"properties": {
"distance_overtaker_mean": segment.distance_overtaker_mean,
"distance_overtaker_max": segment.distance_overtaker_max,
"distance_overtaker_median": segment.distance_overtaker_median,
"overtaking_event_count": segment.overtaking_event_count,
"usage_count": segment.usage_count,
"distance_overtaker_array": segment.distance_overtaker_array,
"direction": segment.direction,
"way_id": segment.way_id,
"zone": segment.zone,
},
}
)
geojson = {"type": "FeatureCollection", "features": features}
return json_response(geojson)
raise InvalidUsage("unknown export format")

View file

@ -104,7 +104,7 @@ const BoundingBoxSelector = React.forwardRef(
} }
); );
const MODES = ["events"]; const MODES = ["events", "segments"];
const FORMATS = ["geojson", "shapefile"]; const FORMATS = ["geojson", "shapefile"];
export default function ExportPage() { export default function ExportPage() {
@ -112,7 +112,6 @@ export default function ExportPage() {
const [bbox, setBbox] = useState("8.294678,49.651182,9.059601,50.108249"); const [bbox, setBbox] = useState("8.294678,49.651182,9.059601,50.108249");
const [fmt, setFmt] = useState("geojson"); const [fmt, setFmt] = useState("geojson");
const config = useConfig(); const config = useConfig();
const exportUrl = `${config?.apiUrl}/export/events?bbox=${bbox}&fmt=${fmt}`;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Page title="Export"> <Page title="Export">
@ -163,7 +162,7 @@ export default function ExportPage() {
<Button <Button
primary primary
as="a" as="a"
href={exportUrl} href={`${config?.apiUrl}/export/${mode}?bbox=${bbox}&fmt=${fmt}`}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >

View file

@ -95,6 +95,7 @@ ExportPage:
label: Modus label: Modus
placeholder: Modus wählen placeholder: Modus wählen
events: Überholvorgänge events: Überholvorgänge
segments: Straßenstatistiken
format: format:
label: Format label: Format
placeholder: Format wählen placeholder: Format wählen

View file

@ -101,6 +101,7 @@ ExportPage:
label: Mode label: Mode
placeholder: Select mode placeholder: Select mode
events: Events events: Events
segments: Road statistics
format: format:
label: Format label: Format
placeholder: Select format placeholder: Select format

View file

@ -102,6 +102,7 @@ ExportPage:
label: Mode label: Mode
placeholder: Sélectionner un mode placeholder: Sélectionner un mode
events: Evénements events: Evénements
segments: Statistiques des rues
format: format:
label: Format label: Format
placeholder: Sélectionner un format placeholder: Sélectionner un format