initial commit

main
yfe404 2023-06-29 06:44:46 +02:00
commit 7f52695a08
11 changed files with 14357 additions and 0 deletions

16
Dockerfile 100644
View File

@ -0,0 +1,16 @@
FROM python:3.9.9-slim-buster
LABEL maintainer="Yann Feunteun <yann.feunteun@protonmail.com>"
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
COPY ./ /app/
EXPOSE 8000
# command to run on container start
CMD [ "uvicorn", "app.main:app", "--host", "0.0.0.0", "--reload" ]

0
app/__init__.py 100644
View File

20
app/config.py 100644
View File

@ -0,0 +1,20 @@
from pydantic import BaseSettings
class Settings(BaseSettings):
grpc_server_host: str = "127.0.0.1"
grpc_server_port: str = "50051"
# Defines the maximum allowed distance in meters
# of a route. Above this threshold, a 404 response code
# will be returned meaning no route correspond to the request.
max_route_length_allowed: int = 20000
# Maximum allowed distance in meters between a requested route
# start/end point and the closest point on our graph
closest_point_tolerance: int = 150
class Config:
env_file = ".env"
# reads env variables (case insensitive) and validate against schema
settings = Settings()

262
app/main.py 100644
View File

@ -0,0 +1,262 @@
import sys
import json
import time
import hashlib
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
from fastapi.middleware.cors import CORSMiddleware
import requests
from .config import settings
import numpy as np
from scipy.spatial import KDTree
from scipy.spatial import ConvexHull
import grpc
from . import route_service_pb2
from . route_service_pb2_grpc import RouteServiceStub
from .schemas import (
Message,
Point,
LineString,
Polygon,
PolygonGeometry,
RouteRequest,
CoverageResponse,
RouteResponsePath
)
from andyamo import types
from geopy import distance as geo_distance
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.utils import get_openapi
from fastapi import Security, Depends, FastAPI, HTTPException
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from starlette.status import HTTP_403_FORBIDDEN
from starlette.responses import RedirectResponse, JSONResponse
channel = grpc.insecure_channel(f"{settings.grpc_server_host}:{settings.grpc_server_port}")
grpc_client = RouteServiceStub(channel)
def get_hash(data: str):
return hashlib.md5(f"{data}".encode("utf-8")).hexdigest()
with open("export_clean_splitted.json", "r") as f:
data = [json.loads(x.strip()) for x in f.readlines()]
hash_to_type = {}
for feature in data:
hash_to_type[feature["feature"]["properties"]["index"]] = feature["feature"]["properties"]["type"]
datastore = {}
for _profile in ["foot", "manual_wheelchair", "electric_wheelchair"]:#, "visually_impaired_cane", "cognitive_disability", "elderly", "hearing_impaired"]:
datastore[_profile] = {}
datastore[_profile]["points"] = []
datastore[_profile]["ids"] = []
req_data = {"profile": types.Profile(_profile).name}
request = route_service_pb2.Profile(**req_data)
for node in grpc_client.ListNodes(request):
#print(node.index, node.longitude, node.latitude)
datastore[_profile]["points"].append([node.longitude, node.latitude])
datastore[_profile]["ids"].append(node.index)
datastore[_profile]["kdtree"] = KDTree(datastore[_profile]["points"])
nodeid_to_coords = {}
for feature in data:
nodeid_to_coords["d_" + feature['feature']["properties"]["nodes"][0]] = \
feature['feature']["geometry"]['coordinates'][0]
nodeid_to_coords["d_" + feature['feature']["properties"]["nodes"][1]] = \
feature['feature']["geometry"]['coordinates'][1]
def compute_convex_hull():
_points = np.array(datastore["foot"]["points"])
hull = ConvexHull(_points)
hull_geojson = {
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
]
}
}
l = []
for vertex in hull.vertices:
l.append(list(datastore["foot"]["points"][vertex]))
l.append(list(datastore["foot"]["points"][hull.vertices[0]]))
hull_geojson["geometry"]["coordinates"] = [l]
return hull_geojson
hull_geojson = compute_convex_hull()
def compute_coverage(features: list) -> tuple:
"""Return bounding box of the geographical area covered by this instance."""
min_lon = sys.maxsize
min_lat = sys.maxsize
max_lon = 0
max_lat = 0
for feature in data:
for (lon, lat) in feature["feature"]["geometry"]["coordinates"]:
if lon < min_lon:
min_lon = lon
if lon > max_lon:
max_lon = lon
if lat < min_lat:
min_lat = lat
if lat > max_lat:
max_lat = lat
return min_lon,min_lat,max_lon,max_lat
def get_point_from_nodeid(nodeid):
return nodeid_to_coords[nodeid]
app = FastAPI()
class ConvexHullResponse(BaseModel):
polygon: Polygon = Field(..., title="polygon", description="Similar to /coverage, returns the convex hull corresponding to the geographical area covered as a GeoJSON polygon.")
@app.get("/healthz", summary="Check if API is up or not")
def healthz():
return { "healthy": "OK" }
@app.get("/coverage", summary="Coverage information (bounding box)", response_model=CoverageResponse)
def coverage():
bbox = compute_coverage(data)
return { "bbox": bbox }
@app.get("/convex", summary="Coverage information (convex hull)", response_model=ConvexHullResponse)
def coverage():
return { "polygon": hull_geojson }
@app.post(
"/route",
summary="Pedestrian routing",
response_model=RouteResponsePath,
responses={
400: {"model": Message},
404: {"model": Message}
}
)
def route(route: RouteRequest = Body(...)) -> RouteResponsePath:
if route.profile.value not in ["foot", "manual_wheelchair", "electric_wheelchair"]:
return JSONResponse(
status_code=404, content={
"message": "Profile not available yet. Available profiles are: foot, manual_wheelchair and electric_wheelchair"
}
)
_, idx_start = datastore[route.profile.value]["kdtree"]\
.query([route.start.lon, route.start.lat])
_, idx_end = datastore[route.profile.value]["kdtree"]\
.query([route.end.lon, route.end.lat])
too_far_from_graph = False
if geo_distance.distance(
get_point_from_nodeid(str(datastore[route.profile.value]["ids"][idx_start])[1:]),
[route.start.lon, route.start.lat]
).m > settings.closest_point_tolerance:
too_far_from_graph = True
if geo_distance.distance(
get_point_from_nodeid(str(datastore[route.profile.value]["ids"][idx_end])[1:]),
[route.end.lon, route.end.lat]
).m > settings.closest_point_tolerance:
too_far_from_graph = True
if too_far_from_graph:
return JSONResponse(
status_code=400, content={
"message": "Start and or end point outside covered perimeter"
}
)
req_data = {
"start": str(datastore[route.profile.value]["ids"][idx_start]),
"end": str(datastore[route.profile.value]["ids"][idx_end]),
"profile": route_service_pb2.Profile(profile=route.profile.name)
}
request = route_service_pb2.RouteRequest(**req_data)
response = grpc_client.ShortestPath(request)
route = response.route
distance = response.distance
if distance > settings.max_route_length_allowed:
return JSONResponse(status_code=404, content={"message": "No route found."})
route_points = [get_point_from_nodeid(x[1:]) for x in route]
segment_types = []
for i in range(len(route_points) - 1):
segment_hash0 = get_hash([route_points[i], route_points[i+1]])
segment_hash1 = get_hash([route_points[i+1], route_points[i]])
if segment_hash0 in hash_to_type.keys():
segment_types.append(hash_to_type[segment_hash0])
elif segment_hash1 in hash_to_type.keys():
segment_types.append(hash_to_type[segment_hash1])
if len(segment_types) < i+1:
segment_types.append("unknown")
min_lon = min([x[0] for x in route_points])
min_lat = min([x[1] for x in route_points])
max_lon = max([x[0] for x in route_points])
max_lat = max([x[1] for x in route_points])
walking_speed = 4000 / 3600 ## 4km/h == 4000 / 3600 m / s
return {
"distance": distance,
"duration": distance * walking_speed,
"points": {
"type": "LineString",
"coordinates": route_points
},
"segment_types": segment_types,
"bbox": [min_lon, min_lat, max_lon, max_lat]
}
return JSONResponse(status_code=404, content={"message": "No route found."})

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: route_service.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13route_service.proto\"E\n\x0cRouteRequest\x12\r\n\x05start\x18\x01 \x01(\t\x12\x0b\n\x03\x65nd\x18\x02 \x01(\t\x12\x19\n\x07profile\x18\x03 \x01(\x0b\x32\x08.Profile\"q\n\x07Profile\x12!\n\x07profile\x18\x01 \x01(\x0e\x32\x10.Profile.Profile\"C\n\x07Profile\x12\x08\n\x04\x46OOT\x10\x00\x12\x15\n\x11MANUAL_WHEELCHAIR\x10\x01\x12\x17\n\x13\x45LECTRIC_WHEELCHAIR\x10\x02\"0\n\rRouteResponse\x12\r\n\x05route\x18\x01 \x03(\t\x12\x10\n\x08\x64istance\x18\x02 \x01(\x05\":\n\x04Node\x12\r\n\x05index\x18\x01 \x01(\t\x12\x10\n\x08latitude\x18\x02 \x01(\x02\x12\x11\n\tlongitude\x18\x03 \x01(\x02\x32\x61\n\x0cRouteService\x12/\n\x0cShortestPath\x12\r.RouteRequest\x1a\x0e.RouteResponse\"\x00\x12 \n\tListNodes\x12\x08.Profile\x1a\x05.Node\"\x00\x30\x01\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'route_service_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_ROUTEREQUEST._serialized_start=23
_ROUTEREQUEST._serialized_end=92
_PROFILE._serialized_start=94
_PROFILE._serialized_end=207
_PROFILE_PROFILE._serialized_start=140
_PROFILE_PROFILE._serialized_end=207
_ROUTERESPONSE._serialized_start=209
_ROUTERESPONSE._serialized_end=257
_NODE._serialized_start=259
_NODE._serialized_end=317
_ROUTESERVICE._serialized_start=319
_ROUTESERVICE._serialized_end=416
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,102 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import route_service_pb2 as route__service__pb2
class RouteServiceStub(object):
"""Interface exported by the server.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.ShortestPath = channel.unary_unary(
'/RouteService/ShortestPath',
request_serializer=route__service__pb2.RouteRequest.SerializeToString,
response_deserializer=route__service__pb2.RouteResponse.FromString,
)
self.ListNodes = channel.unary_stream(
'/RouteService/ListNodes',
request_serializer=route__service__pb2.Profile.SerializeToString,
response_deserializer=route__service__pb2.Node.FromString,
)
class RouteServiceServicer(object):
"""Interface exported by the server.
"""
def ShortestPath(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ListNodes(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_RouteServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'ShortestPath': grpc.unary_unary_rpc_method_handler(
servicer.ShortestPath,
request_deserializer=route__service__pb2.RouteRequest.FromString,
response_serializer=route__service__pb2.RouteResponse.SerializeToString,
),
'ListNodes': grpc.unary_stream_rpc_method_handler(
servicer.ListNodes,
request_deserializer=route__service__pb2.Profile.FromString,
response_serializer=route__service__pb2.Node.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'RouteService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class RouteService(object):
"""Interface exported by the server.
"""
@staticmethod
def ShortestPath(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/RouteService/ShortestPath',
route__service__pb2.RouteRequest.SerializeToString,
route__service__pb2.RouteResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def ListNodes(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(request, target, '/RouteService/ListNodes',
route__service__pb2.Profile.SerializeToString,
route__service__pb2.Node.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

173
app/schemas.py 100644
View File

@ -0,0 +1,173 @@
from andyamo import types
from pydantic import BaseModel, Field
class Message(BaseModel):
message: str
class Point(BaseModel):
lon: float
lat: float
class Config:
schema_extra = {
"example": {
"lon": 3.127781750469675,
"lat": 45.76400009445323
}
}
class LineString(BaseModel):
type: str
coordinates: list[list[float]]
class RouteRequest(BaseModel):
start: Point = Field(..., description="The starting point for which the route should be calculated.")
end: Point = Field(..., description="The ending point for which the route should be calculated.")
profile: types.Profile = Field(..., description="See [availables profiles](#section/Routing-Profiles)")
class CoverageResponse(BaseModel):
bbox: list[float] = Field(..., title="bbox", description="The bounding box of the geographical area covered. Format: [minLon,minLat,maxLon,maxLat]")
class Config:
schema_extra = {
"example": {
"bbox": [
3.073747158050537,
45.77367083508177,
3.075887560844421,
45.77517870042004
]
}
}
class RouteResponsePath(BaseModel):
distance: int = Field(..., description="Total distance in meters for the route returned")
duration: int = Field(..., description="Estimated travel time in seconds. Depends on the chosen [profile](#section/Routing-Profiles)")
points: LineString = Field(..., description="The geometry of the route in [GeoJSON format](https://datatracker.ietf.org/doc/html/rfc7946)")
bbox: list[float] = Field(..., title="bbox", description="The bounding box of the route geometry. Format: [minLon,minLat,maxLon,maxLat]")
segment_types: list[str] = Field(..., description="Segment types in the returned route result. Can be sidewalk, stair, crosswalk or unknown")
class Config:
schema_extra = {
"example": {
"distance": 280,
"duration": 201,
"points": {
"type": "LineString",
"coordinates": [
[
3.073747158050537,
45.77517870042004
],
[
3.0739670991897583,
45.77472597094132
],
[
3.0742889642715454,
45.77413105636887
],
[
3.0747556686401367,
45.77379056781466
],
[
3.074975609779358,
45.77367083508177
],
[
3.0753082036972046,
45.77374940971673
],
[
3.0754369497299194,
45.77386540064218
],
[
3.0756354331970215,
45.77381301767344
],
[
3.075887560844421,
45.773872883919395
]
]
},
"segment_types": ["sidewalk", "sidewalk", "crosswalk", "sidewalk", "sidewalk", "stair", "sidewalk", "sidewalk"],
"bbox": [3.073747158050537, 45.77367083508177, 3.075887560844421, 45.77517870042004]
}
}
class PolygonGeometry(BaseModel):
type: str
coordinates: list[list[list[float]]]
class Config:
schema_extra = {
"example": {
"type": "Polygon",
"coordinates": [
[
[
3.4716796874999996,
47.37603463349758
],
[
3.49639892578125,
47.33603074146188
],
[
3.6488342285156246,
47.429945332976125
],
[
3.4716796874999996,
47.37603463349758
]
]
]
}
}
class Polygon(BaseModel):
type: str
properties: dict
geometry: PolygonGeometry
class Config:
schema_extra = {
"example": {
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
3.4716796874999996,
47.37603463349758
],
[
3.49639892578125,
47.33603074146188
],
[
3.6488342285156246,
47.429945332976125
],
[
3.4716796874999996,
47.37603463349758
]
]
]
}
}
}

File diff suppressed because it is too large Load Diff

13
requirements.txt 100644
View File

@ -0,0 +1,13 @@
-i https://pypi.andyamo.fr/simple
andyamo==0.1.2
fastapi==0.71.0
geopy==2.2.0
uvicorn==0.15.0
requests==2.27.1
grpcio-tools==1.43.0
opentracing==2.4.0
scipy==1.8.0
opentelemetry-sdk==1.10.0
opentelemetry-api==1.10.0
opentelemetry-exporter-jaeger==1.10.0
numpy==1.22.2

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: route_service.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13route_service.proto\"E\n\x0cRouteRequest\x12\r\n\x05start\x18\x01 \x01(\t\x12\x0b\n\x03\x65nd\x18\x02 \x01(\t\x12\x19\n\x07profile\x18\x03 \x01(\x0b\x32\x08.Profile\"q\n\x07Profile\x12!\n\x07profile\x18\x01 \x01(\x0e\x32\x10.Profile.Profile\"C\n\x07Profile\x12\x08\n\x04\x46OOT\x10\x00\x12\x15\n\x11MANUAL_WHEELCHAIR\x10\x01\x12\x17\n\x13\x45LECTRIC_WHEELCHAIR\x10\x02\"0\n\rRouteResponse\x12\r\n\x05route\x18\x01 \x03(\t\x12\x10\n\x08\x64istance\x18\x02 \x01(\x05\":\n\x04Node\x12\r\n\x05index\x18\x01 \x01(\t\x12\x10\n\x08latitude\x18\x02 \x01(\x02\x12\x11\n\tlongitude\x18\x03 \x01(\x02\x32\x61\n\x0cRouteService\x12/\n\x0cShortestPath\x12\r.RouteRequest\x1a\x0e.RouteResponse\"\x00\x12 \n\tListNodes\x12\x08.Profile\x1a\x05.Node\"\x00\x30\x01\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'route_service_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_ROUTEREQUEST._serialized_start=23
_ROUTEREQUEST._serialized_end=92
_PROFILE._serialized_start=94
_PROFILE._serialized_end=207
_PROFILE_PROFILE._serialized_start=140
_PROFILE_PROFILE._serialized_end=207
_ROUTERESPONSE._serialized_start=209
_ROUTERESPONSE._serialized_end=257
_NODE._serialized_start=259
_NODE._serialized_end=317
_ROUTESERVICE._serialized_start=319
_ROUTESERVICE._serialized_end=416
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,102 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import route_service_pb2 as route__service__pb2
class RouteServiceStub(object):
"""Interface exported by the server.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.ShortestPath = channel.unary_unary(
'/RouteService/ShortestPath',
request_serializer=route__service__pb2.RouteRequest.SerializeToString,
response_deserializer=route__service__pb2.RouteResponse.FromString,
)
self.ListNodes = channel.unary_stream(
'/RouteService/ListNodes',
request_serializer=route__service__pb2.Profile.SerializeToString,
response_deserializer=route__service__pb2.Node.FromString,
)
class RouteServiceServicer(object):
"""Interface exported by the server.
"""
def ShortestPath(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ListNodes(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_RouteServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'ShortestPath': grpc.unary_unary_rpc_method_handler(
servicer.ShortestPath,
request_deserializer=route__service__pb2.RouteRequest.FromString,
response_serializer=route__service__pb2.RouteResponse.SerializeToString,
),
'ListNodes': grpc.unary_stream_rpc_method_handler(
servicer.ListNodes,
request_deserializer=route__service__pb2.Profile.FromString,
response_serializer=route__service__pb2.Node.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'RouteService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class RouteService(object):
"""Interface exported by the server.
"""
@staticmethod
def ShortestPath(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/RouteService/ShortestPath',
route__service__pb2.RouteRequest.SerializeToString,
route__service__pb2.RouteResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def ListNodes(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(request, target, '/RouteService/ListNodes',
route__service__pb2.Profile.SerializeToString,
route__service__pb2.Node.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)