EvaluateRouteView
Bases: LoggingMixin
, APIView
Source code in polarrouteserver/route_api/views.py
class EvaluateRouteView(LoggingMixin, APIView):
serializer_class = None
@extend_schema(
operation_id="api_route_evaluation",
request=inline_serializer(
name="RouteEvaluationRequest",
fields={
"route": serializers.JSONField(help_text="The route JSON to evaluate."),
"custom_mesh_id": serializers.UUIDField(
required=False,
allow_null=True,
help_text="Optional: Custom mesh ID to use for evaluation.",
),
},
),
responses={
200: OpenApiResponse(
response=inline_serializer(
name="RouteEvaluationSuccess",
fields={
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"evaluation_results": serializers.DictField(
help_text="Results of the route evaluation."
),
},
),
description="Route evaluated successfully.",
),
404: noMeshOpenApiResponse,
},
)
def post(self, request):
"POST Endpoint to evaluate traveltime and fuel usage on a given route."
data = request.data
route_json = data.get("route", None)
custom_mesh_id = data.get("custom_mesh_id", None)
if custom_mesh_id:
try:
mesh = Mesh.objects.get(id=custom_mesh_id)
meshes = [mesh]
except Mesh.DoesNotExist:
return noMeshResponse()
else:
meshes = select_mesh_for_route_evaluation(route_json)
if meshes is None:
return noMeshResponse()
response_data = {"polarrouteserver-version": polarrouteserver_version}
result_dict = evaluate_route(route_json, meshes[0])
response_data.update(result_dict)
return Response(
response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
post(request)
POST Endpoint to evaluate traveltime and fuel usage on a given route.
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_route_evaluation",
request=inline_serializer(
name="RouteEvaluationRequest",
fields={
"route": serializers.JSONField(help_text="The route JSON to evaluate."),
"custom_mesh_id": serializers.UUIDField(
required=False,
allow_null=True,
help_text="Optional: Custom mesh ID to use for evaluation.",
),
},
),
responses={
200: OpenApiResponse(
response=inline_serializer(
name="RouteEvaluationSuccess",
fields={
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"evaluation_results": serializers.DictField(
help_text="Results of the route evaluation."
),
},
),
description="Route evaluated successfully.",
),
404: noMeshOpenApiResponse,
},
)
def post(self, request):
"POST Endpoint to evaluate traveltime and fuel usage on a given route."
data = request.data
route_json = data.get("route", None)
custom_mesh_id = data.get("custom_mesh_id", None)
if custom_mesh_id:
try:
mesh = Mesh.objects.get(id=custom_mesh_id)
meshes = [mesh]
except Mesh.DoesNotExist:
return noMeshResponse()
else:
meshes = select_mesh_for_route_evaluation(route_json)
if meshes is None:
return noMeshResponse()
response_data = {"polarrouteserver-version": polarrouteserver_version}
result_dict = evaluate_route(route_json, meshes[0])
response_data.update(result_dict)
return Response(
response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
JobView
Bases: LoggingMixin
, GenericAPIView
View for handling job status requests
Source code in polarrouteserver/route_api/views.py
class JobView(LoggingMixin, GenericAPIView):
"""
View for handling job status requests
"""
serializer_class = JobStatusSerializer
@extend_schema(
operation_id="api_job_retrieve_status",
responses={
200: OpenApiResponse(
response=inline_serializer(
name="JobStatusResponse",
fields={
"id": serializers.UUIDField(help_text="ID of the job."),
"status": serializers.ChoiceField(
choices=[
(
"PENDING",
"Task is waiting for execution or unknown task id",
),
("STARTED", "Task has been started"),
("SUCCESS", "Task executed successfully"),
("FAILURE", "Task failed with an exception"),
("RETRY", "Task is being retried after failure"),
("REVOKED", "Task was revoked/cancelled"),
],
help_text="Current status of the job. These are standard Celery task states.",
),
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"route_id": serializers.UUIDField(
help_text="ID of the associated route."
),
"created": serializers.DateTimeField(
help_text="When the job was created."
),
"info": serializers.DictField(
required=False,
help_text="Additional information or error details if status is FAILURE.",
),
"route_url": serializers.URLField(
required=False,
help_text="URL to retrieve the route data when status is SUCCESS.",
),
},
),
description="Job status retrieved successfully.",
),
404: OpenApiResponse(
response=inline_serializer(
name="JobNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating job not found."
)
},
),
description="Job with the specified ID not found.",
),
},
)
def get(self, request, id):
"""Return status of job and route URL if complete."""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
job = Job.objects.get(id=id)
except Job.DoesNotExist:
return Response(
{"error": f"Job with id {id} not found."},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
serializer = JobStatusSerializer(job, context={"request": request})
return Response(
serializer.data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
@extend_schema(
operation_id="api_job_cancel",
responses={
202: OpenApiResponse(
response=inline_serializer(
name="JobCancelAccepted",
fields={
"message": serializers.CharField(
help_text="Confirmation message that job cancellation was accepted."
)
},
),
description="Job cancellation accepted.",
),
404: OpenApiResponse(
response=inline_serializer(
name="JobCancelNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating job not found."
)
},
),
description="Job with the specified ID not found.",
),
},
)
def delete(self, request, id):
"""Cancel job"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
job = Job.objects.get(id=id)
except Job.DoesNotExist:
return Response(
{"error": f"Job with id {id} not found."},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
# Store route ID for response before deletion
route_id = job.route.id
# Cancel the Celery task
result = AsyncResult(id=str(id), app=app)
result.revoke()
# Delete the corresponding route (this will also delete the job due to CASCADE)
job.route.delete()
return Response(
{
"message": f"Job {id} cancellation requested and route {route_id} deleted.",
"job_id": str(job.id),
"route_id": route_id,
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_202_ACCEPTED,
)
delete(request, id)
Cancel job
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_job_cancel",
responses={
202: OpenApiResponse(
response=inline_serializer(
name="JobCancelAccepted",
fields={
"message": serializers.CharField(
help_text="Confirmation message that job cancellation was accepted."
)
},
),
description="Job cancellation accepted.",
),
404: OpenApiResponse(
response=inline_serializer(
name="JobCancelNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating job not found."
)
},
),
description="Job with the specified ID not found.",
),
},
)
def delete(self, request, id):
"""Cancel job"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
job = Job.objects.get(id=id)
except Job.DoesNotExist:
return Response(
{"error": f"Job with id {id} not found."},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
# Store route ID for response before deletion
route_id = job.route.id
# Cancel the Celery task
result = AsyncResult(id=str(id), app=app)
result.revoke()
# Delete the corresponding route (this will also delete the job due to CASCADE)
job.route.delete()
return Response(
{
"message": f"Job {id} cancellation requested and route {route_id} deleted.",
"job_id": str(job.id),
"route_id": route_id,
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_202_ACCEPTED,
)
get(request, id)
Return status of job and route URL if complete.
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_job_retrieve_status",
responses={
200: OpenApiResponse(
response=inline_serializer(
name="JobStatusResponse",
fields={
"id": serializers.UUIDField(help_text="ID of the job."),
"status": serializers.ChoiceField(
choices=[
(
"PENDING",
"Task is waiting for execution or unknown task id",
),
("STARTED", "Task has been started"),
("SUCCESS", "Task executed successfully"),
("FAILURE", "Task failed with an exception"),
("RETRY", "Task is being retried after failure"),
("REVOKED", "Task was revoked/cancelled"),
],
help_text="Current status of the job. These are standard Celery task states.",
),
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"route_id": serializers.UUIDField(
help_text="ID of the associated route."
),
"created": serializers.DateTimeField(
help_text="When the job was created."
),
"info": serializers.DictField(
required=False,
help_text="Additional information or error details if status is FAILURE.",
),
"route_url": serializers.URLField(
required=False,
help_text="URL to retrieve the route data when status is SUCCESS.",
),
},
),
description="Job status retrieved successfully.",
),
404: OpenApiResponse(
response=inline_serializer(
name="JobNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating job not found."
)
},
),
description="Job with the specified ID not found.",
),
},
)
def get(self, request, id):
"""Return status of job and route URL if complete."""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
job = Job.objects.get(id=id)
except Job.DoesNotExist:
return Response(
{"error": f"Job with id {id} not found."},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
serializer = JobStatusSerializer(job, context={"request": request})
return Response(
serializer.data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
LoggingMixin
Provides full logging of requests and responses
Source code in polarrouteserver/route_api/views.py
class LoggingMixin:
"""
Provides full logging of requests and responses
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = logging.getLogger("django.request")
def initial(self, request, *args, **kwargs):
try:
self.logger.debug(
{
"request": request.data,
"method": request.method,
"endpoint": request.path,
"user": request.user.username,
"ip_address": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
}
)
except Exception:
self.logger.exception("Error logging request data")
super().initial(request, *args, **kwargs)
def finalize_response(self, request, response, *args, **kwargs):
try:
self.logger.debug(
{
"response": response.data,
"status_code": response.status_code,
"user": request.user.username,
"ip_address": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
}
)
except Exception:
self.logger.exception("Error logging response data")
return super().finalize_response(request, response, *args, **kwargs)
MeshView
Bases: LoggingMixin
, APIView
Source code in polarrouteserver/route_api/views.py
class MeshView(LoggingMixin, APIView):
serializer_class = None
@extend_schema(
operation_id="api_mesh_get",
responses={
200: OpenApiResponse(
response=inline_serializer(
name="MeshDetailSuccess",
fields={
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"id": serializers.UUIDField(help_text="ID of the mesh."),
"json": serializers.JSONField(help_text="Mesh JSON."),
"geojson": serializers.JSONField(help_text="Mesh GeoJSON."),
},
),
description="Mesh details retrieved successfully.",
),
404: noMeshOpenApiResponse,
},
)
def get(self, request, id):
"GET Meshes by id"
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
data = {"polarrouteserver-version": polarrouteserver_version}
try:
mesh = Mesh.objects.get(id=id)
data.update(
dict(
id=mesh.id,
json=mesh.json,
geojson=EnvironmentMesh.load_from_json(mesh.json).to_geojson(),
)
)
status = rest_framework.status.HTTP_200_OK
except Mesh.DoesNotExist:
status = rest_framework.status.HTTP_404_NOT_FOUND
return Response(
data,
headers={"Content-Type": "application/json"},
status=status,
)
get(request, id)
GET Meshes by id
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_mesh_get",
responses={
200: OpenApiResponse(
response=inline_serializer(
name="MeshDetailSuccess",
fields={
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"id": serializers.UUIDField(help_text="ID of the mesh."),
"json": serializers.JSONField(help_text="Mesh JSON."),
"geojson": serializers.JSONField(help_text="Mesh GeoJSON."),
},
),
description="Mesh details retrieved successfully.",
),
404: noMeshOpenApiResponse,
},
)
def get(self, request, id):
"GET Meshes by id"
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
data = {"polarrouteserver-version": polarrouteserver_version}
try:
mesh = Mesh.objects.get(id=id)
data.update(
dict(
id=mesh.id,
json=mesh.json,
geojson=EnvironmentMesh.load_from_json(mesh.json).to_geojson(),
)
)
status = rest_framework.status.HTTP_200_OK
except Mesh.DoesNotExist:
status = rest_framework.status.HTTP_404_NOT_FOUND
return Response(
data,
headers={"Content-Type": "application/json"},
status=status,
)
RecentRoutesView
Bases: LoggingMixin
, GenericAPIView
Source code in polarrouteserver/route_api/views.py
class RecentRoutesView(LoggingMixin, GenericAPIView):
serializer_class = RouteSerializer
@extend_schema(
operation_id="api_recent_routes_list",
responses={
200: OpenApiResponse(
response=inline_serializer(
name="RecentRoutesSuccess",
fields={
"routes": serializers.ListField(
child=RouteSerializer(),
help_text="List of recent routes.",
),
"polarrouteserver-version": serializers.CharField(),
},
),
description="List of recent routes retrieved successfully.",
),
204: OpenApiResponse(
response=inline_serializer(
name="NoRecentRoutesFound",
fields={
"message": serializers.CharField(
help_text="Message indicating no recent routes were found."
),
},
),
description="No recent routes found for today.",
),
},
)
def get(self, request):
"""Get recent routes"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
# Only get today's routes
routes_today = Route.objects.filter(
calculated__date=datetime.now().date()
).order_by("-calculated")
logger.debug(f"Found {len(routes_today)} routes calculated today.")
if not routes_today.exists():
return Response(
{
"message": "No recent routes found for today.",
"polarrouteserver-version": polarrouteserver_version,
},
status=rest_framework.status.HTTP_204_NO_CONTENT,
)
jobs_data = []
# Process each route to get associated job data
for route in routes_today:
# Get the most recent job for this route
job = route.job_set.latest("datetime")
# Use JobStatusSerializer for consistent job data formatting
job_serializer = JobStatusSerializer(job, context={"request": request})
job_data = job_serializer.data
# Add additional fields specific to recent routes view
job_data.update(
{
"job_id": job.id,
"start_lat": job.route.start_lat,
"start_lon": job.route.start_lon,
"end_lat": job.route.end_lat,
"end_lon": job.route.end_lon,
"start_name": job.route.start_name,
"end_name": job.route.end_name,
"job_url": reverse("job_detail", args=[job.id], request=request),
}
)
# Include full route data if job is successful
if job_data.get("status") == "SUCCESS":
route_data = RouteSerializer(job.route).data
job_data.update(route_data)
jobs_data.append(job_data)
response_data = {
"routes": jobs_data,
"polarrouteserver-version": polarrouteserver_version,
}
return Response(
response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
get(request)
Get recent routes
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_recent_routes_list",
responses={
200: OpenApiResponse(
response=inline_serializer(
name="RecentRoutesSuccess",
fields={
"routes": serializers.ListField(
child=RouteSerializer(),
help_text="List of recent routes.",
),
"polarrouteserver-version": serializers.CharField(),
},
),
description="List of recent routes retrieved successfully.",
),
204: OpenApiResponse(
response=inline_serializer(
name="NoRecentRoutesFound",
fields={
"message": serializers.CharField(
help_text="Message indicating no recent routes were found."
),
},
),
description="No recent routes found for today.",
),
},
)
def get(self, request):
"""Get recent routes"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
# Only get today's routes
routes_today = Route.objects.filter(
calculated__date=datetime.now().date()
).order_by("-calculated")
logger.debug(f"Found {len(routes_today)} routes calculated today.")
if not routes_today.exists():
return Response(
{
"message": "No recent routes found for today.",
"polarrouteserver-version": polarrouteserver_version,
},
status=rest_framework.status.HTTP_204_NO_CONTENT,
)
jobs_data = []
# Process each route to get associated job data
for route in routes_today:
# Get the most recent job for this route
job = route.job_set.latest("datetime")
# Use JobStatusSerializer for consistent job data formatting
job_serializer = JobStatusSerializer(job, context={"request": request})
job_data = job_serializer.data
# Add additional fields specific to recent routes view
job_data.update(
{
"job_id": job.id,
"start_lat": job.route.start_lat,
"start_lon": job.route.start_lon,
"end_lat": job.route.end_lat,
"end_lon": job.route.end_lon,
"start_name": job.route.start_name,
"end_name": job.route.end_name,
"job_url": reverse("job_detail", args=[job.id], request=request),
}
)
# Include full route data if job is successful
if job_data.get("status") == "SUCCESS":
route_data = RouteSerializer(job.route).data
job_data.update(route_data)
jobs_data.append(job_data)
response_data = {
"routes": jobs_data,
"polarrouteserver-version": polarrouteserver_version,
}
return Response(
response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
RouteDetailView
Bases: LoggingMixin
, GenericAPIView
Source code in polarrouteserver/route_api/views.py
class RouteDetailView(LoggingMixin, GenericAPIView):
serializer_class = RouteSerializer
@extend_schema(
operation_id="api_route_retrieve_by_id",
responses={
200: OpenApiResponse(
response=RouteSerializer,
description="Route data retrieved successfully.",
),
404: OpenApiResponse(
response=inline_serializer(
name="RouteNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating route not found."
)
},
),
description="Route with the specified ID not found.",
),
},
)
def get(self, request, id):
"""Return route data by route ID."""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
route = Route.objects.get(id=id)
except Route.DoesNotExist:
return Response(
{"error": f"Route with id {id} not found."},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
data = RouteSerializer(route).data
return Response(
data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
get(request, id)
Return route data by route ID.
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_route_retrieve_by_id",
responses={
200: OpenApiResponse(
response=RouteSerializer,
description="Route data retrieved successfully.",
),
404: OpenApiResponse(
response=inline_serializer(
name="RouteNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating route not found."
)
},
),
description="Route with the specified ID not found.",
),
},
)
def get(self, request, id):
"""Return route data by route ID."""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
route = Route.objects.get(id=id)
except Route.DoesNotExist:
return Response(
{"error": f"Route with id {id} not found."},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
data = RouteSerializer(route).data
return Response(
data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
RouteRequestView
Bases: LoggingMixin
, GenericAPIView
Source code in polarrouteserver/route_api/views.py
class RouteRequestView(LoggingMixin, GenericAPIView):
serializer_class = RouteSerializer
@extend_schema(
operation_id="api_route_create_request",
request=inline_serializer(
name="RouteCreationRequest",
# This should be updated along with the json validation below
fields={
"start_lat": serializers.FloatField(
help_text="Starting latitude of the route."
),
"start_lon": serializers.FloatField(
help_text="Starting longitude of the route."
),
"end_lat": serializers.FloatField(
help_text="Ending latitude of the route."
),
"end_lon": serializers.FloatField(
help_text="Ending longitude of the route."
),
"start_name": serializers.CharField(
required=False,
allow_null=True,
help_text="Name of the start point.",
),
"end_name": serializers.CharField(
required=False, allow_null=True, help_text="Name of the end point."
),
"mesh_id": serializers.UUIDField(
required=False,
allow_null=True,
help_text="Optional: Custom mesh ID to use for route calculation.",
),
"force_new_route": serializers.BooleanField(
required=False,
default=False,
help_text="If true, forces recalculation even if an existing route is found.",
),
},
),
responses={
202: OpenApiResponse(
response=inline_serializer(
name="RouteRequestAccepted",
fields={
"job_id": serializers.UUIDField(
help_text="ID of the submitted job for route calculation."
),
"status-url": serializers.URLField(
help_text="URL to check the status of the route calculation job."
),
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"info": serializers.DictField(
required=False,
help_text="Information or warning messages about the route calculation.",
),
},
),
description="Route calculation job submitted successfully.",
),
400: OpenApiResponse(
response=inline_serializer(
name="RouteCreationBadRequest",
fields={
"info": serializers.DictField(
help_text="Details about the error, e.g., missing parameters."
),
"status": serializers.CharField(
help_text="Status of the request (e.g., FAILURE)."
),
},
),
description="Invalid request data.",
),
404: noMeshOpenApiResponse,
},
)
def post(self, request):
"""Entry point for route requests"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}: {request.data}"
)
data = request.data
# TODO validate request JSON
try:
start_lat = float(data["start_lat"])
start_lon = float(data["start_lon"])
end_lat = float(data["end_lat"])
end_lon = float(data["end_lon"])
except (ValueError, TypeError, KeyError) as e:
msg = f"Invalid coordinate values provided: {e}"
logger.error(msg)
return Response(
data={
"info": {"error": msg},
"status": "FAILURE",
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_400_BAD_REQUEST,
)
start_name = data.get("start_name", None)
end_name = data.get("end_name", None)
custom_mesh_id = data.get("mesh_id", None)
force_new_route = data.get("force_new_route", False)
if custom_mesh_id:
try:
logger.info(f"Got custom mesh id {custom_mesh_id} in request.")
meshes = [Mesh.objects.get(id=custom_mesh_id)]
except Mesh.DoesNotExist:
msg = f"Mesh id {custom_mesh_id} requested. Does not exist."
logger.info(msg)
return Response(
data={
"info": {"error": msg},
"status": "FAILURE",
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
else:
meshes = select_mesh(start_lat, start_lon, end_lat, end_lon)
if meshes is None:
return noMeshResponse()
logger.debug(f"Using meshes: {[mesh.id for mesh in meshes]}")
# TODO Future: calculate an up to date mesh if none available
existing_route = route_exists(meshes, start_lat, start_lon, end_lat, end_lon)
if existing_route is not None:
if not force_new_route:
logger.info(f"Existing route found: {existing_route}")
# Check if there's an existing job for this route
existing_job = existing_route.job_set.latest("datetime")
response_data = {
"id": str(existing_job.id),
"status-url": reverse(
"job_detail", args=[existing_job.id], request=request
),
"polarrouteserver-version": polarrouteserver_version,
"info": {
"message": "Pre-existing route found. Job already exists. To force new calculation, include 'force_new_route': true in POST request."
},
}
return Response(
data=response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_202_ACCEPTED,
)
else:
logger.info(
f"Found existing route(s) but got force_new_route={force_new_route}, beginning recalculation."
)
logger.debug(
f"Using mesh {meshes[0].id} as primary mesh with {[mesh.id for mesh in meshes[1:]]} as backup."
)
# Create route in database
route = Route.objects.create(
start_lat=start_lat,
start_lon=start_lon,
end_lat=end_lat,
end_lon=end_lon,
mesh=meshes[0],
start_name=start_name,
end_name=end_name,
)
# Start the task calculation
task = optimise_route.delay(
route.id, backup_mesh_ids=[mesh.id for mesh in meshes[1:]]
)
# Create database record representing the calculation job
job = Job.objects.create(
id=task.id,
route=route,
)
# Prepare response data
data = {
"id": job.id,
"status-url": reverse("job_detail", args=[job.id], request=request),
"polarrouteserver-version": polarrouteserver_version,
}
return Response(
data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_202_ACCEPTED,
)
post(request)
Entry point for route requests
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_route_create_request",
request=inline_serializer(
name="RouteCreationRequest",
# This should be updated along with the json validation below
fields={
"start_lat": serializers.FloatField(
help_text="Starting latitude of the route."
),
"start_lon": serializers.FloatField(
help_text="Starting longitude of the route."
),
"end_lat": serializers.FloatField(
help_text="Ending latitude of the route."
),
"end_lon": serializers.FloatField(
help_text="Ending longitude of the route."
),
"start_name": serializers.CharField(
required=False,
allow_null=True,
help_text="Name of the start point.",
),
"end_name": serializers.CharField(
required=False, allow_null=True, help_text="Name of the end point."
),
"mesh_id": serializers.UUIDField(
required=False,
allow_null=True,
help_text="Optional: Custom mesh ID to use for route calculation.",
),
"force_new_route": serializers.BooleanField(
required=False,
default=False,
help_text="If true, forces recalculation even if an existing route is found.",
),
},
),
responses={
202: OpenApiResponse(
response=inline_serializer(
name="RouteRequestAccepted",
fields={
"job_id": serializers.UUIDField(
help_text="ID of the submitted job for route calculation."
),
"status-url": serializers.URLField(
help_text="URL to check the status of the route calculation job."
),
"polarrouteserver-version": serializers.CharField(
help_text="Version of PolarRoute-server."
),
"info": serializers.DictField(
required=False,
help_text="Information or warning messages about the route calculation.",
),
},
),
description="Route calculation job submitted successfully.",
),
400: OpenApiResponse(
response=inline_serializer(
name="RouteCreationBadRequest",
fields={
"info": serializers.DictField(
help_text="Details about the error, e.g., missing parameters."
),
"status": serializers.CharField(
help_text="Status of the request (e.g., FAILURE)."
),
},
),
description="Invalid request data.",
),
404: noMeshOpenApiResponse,
},
)
def post(self, request):
"""Entry point for route requests"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}: {request.data}"
)
data = request.data
# TODO validate request JSON
try:
start_lat = float(data["start_lat"])
start_lon = float(data["start_lon"])
end_lat = float(data["end_lat"])
end_lon = float(data["end_lon"])
except (ValueError, TypeError, KeyError) as e:
msg = f"Invalid coordinate values provided: {e}"
logger.error(msg)
return Response(
data={
"info": {"error": msg},
"status": "FAILURE",
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_400_BAD_REQUEST,
)
start_name = data.get("start_name", None)
end_name = data.get("end_name", None)
custom_mesh_id = data.get("mesh_id", None)
force_new_route = data.get("force_new_route", False)
if custom_mesh_id:
try:
logger.info(f"Got custom mesh id {custom_mesh_id} in request.")
meshes = [Mesh.objects.get(id=custom_mesh_id)]
except Mesh.DoesNotExist:
msg = f"Mesh id {custom_mesh_id} requested. Does not exist."
logger.info(msg)
return Response(
data={
"info": {"error": msg},
"status": "FAILURE",
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
else:
meshes = select_mesh(start_lat, start_lon, end_lat, end_lon)
if meshes is None:
return noMeshResponse()
logger.debug(f"Using meshes: {[mesh.id for mesh in meshes]}")
# TODO Future: calculate an up to date mesh if none available
existing_route = route_exists(meshes, start_lat, start_lon, end_lat, end_lon)
if existing_route is not None:
if not force_new_route:
logger.info(f"Existing route found: {existing_route}")
# Check if there's an existing job for this route
existing_job = existing_route.job_set.latest("datetime")
response_data = {
"id": str(existing_job.id),
"status-url": reverse(
"job_detail", args=[existing_job.id], request=request
),
"polarrouteserver-version": polarrouteserver_version,
"info": {
"message": "Pre-existing route found. Job already exists. To force new calculation, include 'force_new_route': true in POST request."
},
}
return Response(
data=response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_202_ACCEPTED,
)
else:
logger.info(
f"Found existing route(s) but got force_new_route={force_new_route}, beginning recalculation."
)
logger.debug(
f"Using mesh {meshes[0].id} as primary mesh with {[mesh.id for mesh in meshes[1:]]} as backup."
)
# Create route in database
route = Route.objects.create(
start_lat=start_lat,
start_lon=start_lon,
end_lat=end_lat,
end_lon=end_lon,
mesh=meshes[0],
start_name=start_name,
end_name=end_name,
)
# Start the task calculation
task = optimise_route.delay(
route.id, backup_mesh_ids=[mesh.id for mesh in meshes[1:]]
)
# Create database record representing the calculation job
job = Job.objects.create(
id=task.id,
route=route,
)
# Prepare response data
data = {
"id": job.id,
"status-url": reverse("job_detail", args=[job.id], request=request),
"polarrouteserver-version": polarrouteserver_version,
}
return Response(
data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_202_ACCEPTED,
)
VehicleDetailView
Bases: LoggingMixin
, GenericAPIView
Source code in polarrouteserver/route_api/views.py
class VehicleDetailView(LoggingMixin, GenericAPIView):
serializer_class = VehicleSerializer
@extend_schema(
operation_id="api_vehicle_retrieve_by_type",
responses={
200: OpenApiResponse(
response=VehicleSerializer(many=True),
description="Vehicle details retrieved successfully.",
),
404: OpenApiResponse(
response=inline_serializer(
name="VehicleNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating vehicle not found."
)
},
),
description="Vehicle with the specified vessel_type not found.",
),
},
)
def get(self, request, vessel_type):
"""Retrieve vehicle by vessel_type"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
logger.info(f"Fetching vehicle(s) with vessel_type={vessel_type}")
vehicles = Vehicle.objects.filter(vessel_type=vessel_type)
serializer = self.serializer_class(vehicles, many=True)
return Response(
serializer.data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
@extend_schema(
operation_id="api_vehicle_delete_by_type",
responses={
204: OpenApiResponse(
response=None, description="Vehicle deleted successfully."
),
404: OpenApiResponse(
response=inline_serializer(
name="VehicleDeleteNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating vehicle not found."
)
},
),
description="Vehicle with the specified vessel_type not found.",
),
},
)
def delete(self, request, vessel_type):
"""Delete vehicle by vessel_type"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
vehicle = Vehicle.objects.get(vessel_type=vessel_type)
vehicle.delete()
logger.info(f"Deleted vehicle with vessel_type={vessel_type}")
return Response(
{"message": f"Vehicle '{vessel_type}' deleted successfully."},
status=rest_framework.status.HTTP_204_NO_CONTENT,
)
except Vehicle.DoesNotExist:
logger.error(
f"Vehicle with vessel_type={vessel_type} not found for deletion."
)
return Response(
{"error": f"Vehicle with vessel_type '{vessel_type}' not found."},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
delete(request, vessel_type)
Delete vehicle by vessel_type
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_vehicle_delete_by_type",
responses={
204: OpenApiResponse(
response=None, description="Vehicle deleted successfully."
),
404: OpenApiResponse(
response=inline_serializer(
name="VehicleDeleteNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating vehicle not found."
)
},
),
description="Vehicle with the specified vessel_type not found.",
),
},
)
def delete(self, request, vessel_type):
"""Delete vehicle by vessel_type"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
try:
vehicle = Vehicle.objects.get(vessel_type=vessel_type)
vehicle.delete()
logger.info(f"Deleted vehicle with vessel_type={vessel_type}")
return Response(
{"message": f"Vehicle '{vessel_type}' deleted successfully."},
status=rest_framework.status.HTTP_204_NO_CONTENT,
)
except Vehicle.DoesNotExist:
logger.error(
f"Vehicle with vessel_type={vessel_type} not found for deletion."
)
return Response(
{"error": f"Vehicle with vessel_type '{vessel_type}' not found."},
status=rest_framework.status.HTTP_404_NOT_FOUND,
)
get(request, vessel_type)
Retrieve vehicle by vessel_type
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_vehicle_retrieve_by_type",
responses={
200: OpenApiResponse(
response=VehicleSerializer(many=True),
description="Vehicle details retrieved successfully.",
),
404: OpenApiResponse(
response=inline_serializer(
name="VehicleNotFound",
fields={
"error": serializers.CharField(
help_text="Error message indicating vehicle not found."
)
},
),
description="Vehicle with the specified vessel_type not found.",
),
},
)
def get(self, request, vessel_type):
"""Retrieve vehicle by vessel_type"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
logger.info(f"Fetching vehicle(s) with vessel_type={vessel_type}")
vehicles = Vehicle.objects.filter(vessel_type=vessel_type)
serializer = self.serializer_class(vehicles, many=True)
return Response(
serializer.data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
VehicleRequestView
Bases: LoggingMixin
, GenericAPIView
Source code in polarrouteserver/route_api/views.py
class VehicleRequestView(LoggingMixin, GenericAPIView):
serializer_class = VehicleSerializer
@extend_schema(
operation_id="api_vehicle_create_request",
request=VehicleSerializer,
responses={
200: OpenApiResponse(
response=inline_serializer(
name="VehicleCreationSuccess",
fields={
"vessel_type": serializers.CharField(
help_text="The type of vessel successfully created or updated."
)
},
),
description="Vehicle created or updated successfully.",
),
400: OpenApiResponse(
response=inline_serializer(
name="VehicleValidationError",
fields={
"info": serializers.DictField(
help_text="Details about the validation error, including the error message."
)
},
),
description="Invalid input data for vehicle configuration.",
),
406: OpenApiResponse(
response=inline_serializer(
name="VehicleExistsError",
fields={
"info": serializers.DictField(
help_text="Details about the conflict, indicating a pre-existing vehicle."
)
},
),
description="Pre-existing vehicle found, 'force_properties' not specified or not true.",
),
},
)
def post(self, request):
"""Entry point to create vehicles"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}: {request.data}"
)
data = request.data
# Using Polarroute's built in validation to validate vessel config supplied
try:
validate_vessel_config(data)
logging.info("Vessel config is valid.")
except Exception as e:
if isinstance(e, ValidationError):
error_message = f"Validation error: {e.message}"
else:
error_message = f"{e}"
logging.error(error_message)
return Response(
data={**data, "info": {"error": {error_message}}},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_400_BAD_REQUEST,
)
# Separate out vessel_type and force_properties for checking logic below
force_properties = data.get("force_properties", None)
vessel_type = data["vessel_type"]
# Check if vehicle exists already
vehicle_queryset = Vehicle.objects.filter(vessel_type=vessel_type)
# If the vehicle exists, obtain it and return an error if user has not specified force_properties
if vehicle_queryset.exists():
logger.info(f"Existing vehicle found: {vessel_type}")
if not force_properties:
return Response(
data={
**data,
"info": {
"error": (
"Pre-existing vehicle was found. "
"To force new properties on an existing vehicle, "
"include 'force_properties': true in POST request."
)
},
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_406_NOT_ACCEPTABLE,
)
# If a user has specified force_properties, update that vessel_type's properties
# The vessel_type and force_properties fields need to be removed to allow updating
vehicle_properties = data.copy()
for key in ["vessel_type", "force_properties"]:
vehicle_properties.pop(key, None)
vehicle_queryset.update(**vehicle_properties)
logger.info(f"Updating properties for existing vehicle: {vessel_type}")
response_data = {"vessel_type": vessel_type}
else:
logger.info("Creating new vehicle:")
# Create vehicle in database
vehicle = Vehicle.objects.create(**data)
# Prepare response data
response_data = {"vessel_type": vehicle.vessel_type}
return Response(
response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
@extend_schema(
operation_id="api_vehicle_list_retrieve",
responses={
200: OpenApiResponse(
response=VehicleSerializer(many=True),
description="List of all vehicles.",
),
204: OpenApiResponse(
response=None,
description="No vehicles found.",
),
},
)
def get(self, request):
"""Retrieve all vehicles"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
logger.info("Fetching all vehicles")
vehicles = Vehicle.objects.all()
serializer = self.serializer_class(vehicles, many=True)
return Response(
serializer.data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
get(request)
Retrieve all vehicles
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_vehicle_list_retrieve",
responses={
200: OpenApiResponse(
response=VehicleSerializer(many=True),
description="List of all vehicles.",
),
204: OpenApiResponse(
response=None,
description="No vehicles found.",
),
},
)
def get(self, request):
"""Retrieve all vehicles"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
logger.info("Fetching all vehicles")
vehicles = Vehicle.objects.all()
serializer = self.serializer_class(vehicles, many=True)
return Response(
serializer.data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
post(request)
Entry point to create vehicles
Source code in polarrouteserver/route_api/views.py
@extend_schema(
operation_id="api_vehicle_create_request",
request=VehicleSerializer,
responses={
200: OpenApiResponse(
response=inline_serializer(
name="VehicleCreationSuccess",
fields={
"vessel_type": serializers.CharField(
help_text="The type of vessel successfully created or updated."
)
},
),
description="Vehicle created or updated successfully.",
),
400: OpenApiResponse(
response=inline_serializer(
name="VehicleValidationError",
fields={
"info": serializers.DictField(
help_text="Details about the validation error, including the error message."
)
},
),
description="Invalid input data for vehicle configuration.",
),
406: OpenApiResponse(
response=inline_serializer(
name="VehicleExistsError",
fields={
"info": serializers.DictField(
help_text="Details about the conflict, indicating a pre-existing vehicle."
)
},
),
description="Pre-existing vehicle found, 'force_properties' not specified or not true.",
),
},
)
def post(self, request):
"""Entry point to create vehicles"""
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}: {request.data}"
)
data = request.data
# Using Polarroute's built in validation to validate vessel config supplied
try:
validate_vessel_config(data)
logging.info("Vessel config is valid.")
except Exception as e:
if isinstance(e, ValidationError):
error_message = f"Validation error: {e.message}"
else:
error_message = f"{e}"
logging.error(error_message)
return Response(
data={**data, "info": {"error": {error_message}}},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_400_BAD_REQUEST,
)
# Separate out vessel_type and force_properties for checking logic below
force_properties = data.get("force_properties", None)
vessel_type = data["vessel_type"]
# Check if vehicle exists already
vehicle_queryset = Vehicle.objects.filter(vessel_type=vessel_type)
# If the vehicle exists, obtain it and return an error if user has not specified force_properties
if vehicle_queryset.exists():
logger.info(f"Existing vehicle found: {vessel_type}")
if not force_properties:
return Response(
data={
**data,
"info": {
"error": (
"Pre-existing vehicle was found. "
"To force new properties on an existing vehicle, "
"include 'force_properties': true in POST request."
)
},
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_406_NOT_ACCEPTABLE,
)
# If a user has specified force_properties, update that vessel_type's properties
# The vessel_type and force_properties fields need to be removed to allow updating
vehicle_properties = data.copy()
for key in ["vessel_type", "force_properties"]:
vehicle_properties.pop(key, None)
vehicle_queryset.update(**vehicle_properties)
logger.info(f"Updating properties for existing vehicle: {vessel_type}")
response_data = {"vessel_type": vessel_type}
else:
logger.info("Creating new vehicle:")
# Create vehicle in database
vehicle = Vehicle.objects.create(**data)
# Prepare response data
response_data = {"vessel_type": vehicle.vessel_type}
return Response(
response_data,
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_200_OK,
)
VehicleTypeListView
Bases: LoggingMixin
, GenericAPIView
Endpoint to list all distinct vessel_types available.
Source code in polarrouteserver/route_api/views.py
class VehicleTypeListView(LoggingMixin, GenericAPIView):
"""
Endpoint to list all distinct vessel_types available.
"""
serializer_class = VesselTypeSerializer
@extend_schema(
operation_id="api_vehicle_available_list",
responses={
200: OpenApiResponse(
response=inline_serializer(
name="VesselTypeListSuccess",
fields={
"vessel_types": serializers.ListField(
child=serializers.CharField(),
help_text="List of available vessel types.",
),
},
),
description="List of available vessel types retrieved successfully.",
),
204: OpenApiResponse(
response=inline_serializer(
name="NoVesselTypesFound",
fields={
"vessel_types": serializers.ListField(
child=serializers.CharField(),
help_text="Empty list of vessel types.",
),
"message": serializers.CharField(
help_text="Message indicating no vessel types were found."
),
},
),
description="No available vessel types found.",
),
},
)
def get(self, request):
logger.info(
f"{request.method} {request.path} from {request.META.get('REMOTE_ADDR')}"
)
vessel_types = Vehicle.objects.values_list("vessel_type", flat=True).distinct()
vessel_types_list = list(vessel_types)
if not vessel_types_list:
logger.warning("No available vessel_types found in the database.")
return Response(
data={
"vessel_types": [],
"message": "No available vessel types found.",
},
headers={"Content-Type": "application/json"},
status=rest_framework.status.HTTP_204_NO_CONTENT,
)
logger.info(f"Returning {len(vessel_types_list)} distinct vessel_types")
return Response(
data={"vessel_types": vessel_types_list},
status=rest_framework.status.HTTP_200_OK,
headers={"Content-Type": "application/json"},
)