JobStatusSerializer

Bases: ModelSerializer

Serializer for job status responses with dynamic status and route URL.

The status field returns Celery task states: - 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

Source code in polarrouteserver/route_api/serializers.py
class JobStatusSerializer(serializers.ModelSerializer):
    """
    Serializer for job status responses with dynamic status and route URL.

    The status field returns Celery task states:
    - 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
    """

    status = serializers.SerializerMethodField()
    route_url = serializers.SerializerMethodField()
    info = serializers.SerializerMethodField()
    route_id = serializers.CharField(source="route.id", read_only=True)
    created = serializers.DateTimeField(source="datetime", read_only=True)

    class Meta:
        model = Job
        fields = [
            "id",
            "status",
            "route_id",
            "created",
            "route_url",
            "info",
        ]

    def _get_celery_result(self, obj):
        """Get Celery result object for this job."""
        if not hasattr(self, "_celery_result_cache"):
            self._celery_result_cache = {}

        if obj.id not in self._celery_result_cache:
            self._celery_result_cache[obj.id] = AsyncResult(id=str(obj.id), app=app)

        return self._celery_result_cache[obj.id]

    def get_status(self, obj):
        """Get current job status from Celery."""
        result = self._get_celery_result(obj)
        return result.state

    def get_route_url(self, obj):
        """Include route URL when job is successful."""
        result = self._get_celery_result(obj)
        if result.state == "SUCCESS":
            request = self.context.get("request")
            if request:
                return reverse("route_detail", args=[obj.route.id], request=request)
        return None

    def get_info(self, obj):
        """Include error info when job failed."""
        result = self._get_celery_result(obj)
        if result.state == "FAILURE":
            return {"error": obj.route.info}
        return None

    def to_representation(self, instance):
        """Add version to response."""
        data = super().to_representation(instance)
        data["polarrouteserver-version"] = polarrouteserver_version

        # Remove None values for cleaner response
        return {k: v for k, v in data.items() if v is not None}

get_info(obj)

Include error info when job failed.

Source code in polarrouteserver/route_api/serializers.py
def get_info(self, obj):
    """Include error info when job failed."""
    result = self._get_celery_result(obj)
    if result.state == "FAILURE":
        return {"error": obj.route.info}
    return None

get_route_url(obj)

Include route URL when job is successful.

Source code in polarrouteserver/route_api/serializers.py
def get_route_url(self, obj):
    """Include route URL when job is successful."""
    result = self._get_celery_result(obj)
    if result.state == "SUCCESS":
        request = self.context.get("request")
        if request:
            return reverse("route_detail", args=[obj.route.id], request=request)
    return None

get_status(obj)

Get current job status from Celery.

Source code in polarrouteserver/route_api/serializers.py
def get_status(self, obj):
    """Get current job status from Celery."""
    result = self._get_celery_result(obj)
    return result.state

to_representation(instance)

Add version to response.

Source code in polarrouteserver/route_api/serializers.py
def to_representation(self, instance):
    """Add version to response."""
    data = super().to_representation(instance)
    data["polarrouteserver-version"] = polarrouteserver_version

    # Remove None values for cleaner response
    return {k: v for k, v in data.items() if v is not None}

RouteSerializer

Bases: ModelSerializer

Source code in polarrouteserver/route_api/serializers.py
class RouteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Route
        fields = [
            "id",
            "start_lat",
            "start_lon",
            "end_lat",
            "end_lon",
            "start_name",
            "end_name",
            "json",
            "json_unsmoothed",
            "polar_route_version",
            "info",
            "mesh",
            "requested",
            "calculated",
        ]

    def _extract_routes_by_type(self, route_data, route_type):
        """Extract routes of a specific optimisation type from route data."""
        if route_data is None:
            return []

        return [
            x
            for x in route_data
            if (
                x
                and len(x) > 0
                and isinstance(x[0], dict)
                and x[0].get("features")
                and len(x[0]["features"]) > 0
                and x[0]["features"][0].get("properties", {}).get("objective_function")
                == route_type
            )
        ]

    def _build_optimisation_metrics(self, route_type, properties):
        """Build metrics based on route type and properties."""
        if route_type == "traveltime":
            duration = properties.get("total_traveltime", 0)
            return {"time": {"duration": str(duration)}}
        elif route_type == "fuel":
            return {
                "fuelConsumption": {
                    "value": properties.get("total_fuel"),
                    "units": properties.get("fuel_units") or "tons",
                }
            }
        return {}

    def _build_mesh_info(self, instance):
        """Build mesh information from the route instance."""
        if not instance.mesh:
            return None

        return {
            "id": instance.mesh.id,
            "name": instance.mesh.name,
            "validDateStart": instance.mesh.valid_date_start.isoformat()
            if instance.mesh.valid_date_start
            else None,
            "validDateEnd": instance.mesh.valid_date_end.isoformat()
            if instance.mesh.valid_date_end
            else None,
            "bounds": {
                "latMin": instance.mesh.lat_min,
                "latMax": instance.mesh.lat_max,
                "lonMin": instance.mesh.lon_min,
                "lonMax": instance.mesh.lon_max,
            },
        }

    def to_representation(self, instance):
        """Transform route data into structured format."""
        data = super().to_representation(instance)

        # Extract and organise route data by optimisation type
        smoothed_routes = {}
        unsmoothed_routes = {}

        for route_type in ("traveltime", "fuel"):
            smoothed_routes[route_type] = self._extract_routes_by_type(
                data["json"], route_type
            )
            unsmoothed_routes[route_type] = self._extract_routes_by_type(
                data["json_unsmoothed"], route_type
            )

        # Build structured response for each available route type
        available_routes = []

        for route_type in ("traveltime", "fuel"):
            smoothed = smoothed_routes[route_type]
            unsmoothed = unsmoothed_routes[route_type]

            # Determine which route to use (smoothed preferred, fallback to unsmoothed)
            route_geojson = None
            unsmoothed_geojson = None
            info_message = None

            if len(smoothed) > 0:
                route_geojson = smoothed[0][
                    0
                ]  # Extract the actual GeoJSON from the nested structure
                unsmoothed_geojson = unsmoothed[0][0] if len(unsmoothed) > 0 else None
            elif len(unsmoothed) > 0:
                route_geojson = unsmoothed[0][
                    0
                ]  # Extract the actual GeoJSON from the nested structure
                info_message = {
                    "warning": f"Smoothing failed for {route_type}-optimisation, returning unsmoothed route."
                }
            else:
                # No route available for this type - skip it
                continue

            # Extract optimisation metrics from route properties
            properties = (
                route_geojson["features"][0].get("properties", {})
                if route_geojson
                else {}
            )
            optimisation_metrics = self._build_optimisation_metrics(
                route_type, properties
            )

            # Build mesh information
            mesh_info = self._build_mesh_info(instance)

            # Build structured route object
            route_obj = {
                "type": route_type,
                "id": str(instance.id),
                "name": f"{data.get('start_name') or 'Start'} to {data.get('end_name') or 'End'} ({route_type})",
                "job": {
                    "requestedAt": data["requested"],
                    "calculatedAt": data["calculated"],
                },
                "waypoints": {
                    "start": {
                        "lat": data["start_lat"],
                        "lon": data["start_lon"],
                        "name": data.get("start_name"),
                    },
                    "end": {
                        "lat": data["end_lat"],
                        "lon": data["end_lon"],
                        "name": data.get("end_name"),
                    },
                },
                "path": route_geojson,
                "unsmoothedPath": unsmoothed_geojson,
                "optimisation": {"metrics": optimisation_metrics},
            }

            if mesh_info:
                route_obj["mesh"] = mesh_info

            # Add any info/warnings
            if info_message:
                route_obj["info"] = info_message
            elif data.get("info"):
                route_obj["info"] = data["info"]

            available_routes.append(route_obj)

        # Return the appropriate format
        if len(available_routes) == 0:
            # No routes available - return error
            result = {
                "type": "error",
                "id": str(instance.id),
                "name": f"{data.get('start_name') or 'Start'} to {data.get('end_name') or 'End'}",
                "job": {
                    "requestedAt": data["requested"],
                    "calculatedAt": data["calculated"],
                },
                "info": {"error": "No routes available for any optimisation type."},
            }
        elif len(available_routes) == 1:
            # Single route type - return the route directly
            result = available_routes[0]
        else:
            # Multiple route types - return as array
            result = {"routes": available_routes}

        # Add version to all responses
        if isinstance(result, dict):
            result["polarrouteserver-version"] = polarrouteserver_version

        return result

to_representation(instance)

Transform route data into structured format.

Source code in polarrouteserver/route_api/serializers.py
def to_representation(self, instance):
    """Transform route data into structured format."""
    data = super().to_representation(instance)

    # Extract and organise route data by optimisation type
    smoothed_routes = {}
    unsmoothed_routes = {}

    for route_type in ("traveltime", "fuel"):
        smoothed_routes[route_type] = self._extract_routes_by_type(
            data["json"], route_type
        )
        unsmoothed_routes[route_type] = self._extract_routes_by_type(
            data["json_unsmoothed"], route_type
        )

    # Build structured response for each available route type
    available_routes = []

    for route_type in ("traveltime", "fuel"):
        smoothed = smoothed_routes[route_type]
        unsmoothed = unsmoothed_routes[route_type]

        # Determine which route to use (smoothed preferred, fallback to unsmoothed)
        route_geojson = None
        unsmoothed_geojson = None
        info_message = None

        if len(smoothed) > 0:
            route_geojson = smoothed[0][
                0
            ]  # Extract the actual GeoJSON from the nested structure
            unsmoothed_geojson = unsmoothed[0][0] if len(unsmoothed) > 0 else None
        elif len(unsmoothed) > 0:
            route_geojson = unsmoothed[0][
                0
            ]  # Extract the actual GeoJSON from the nested structure
            info_message = {
                "warning": f"Smoothing failed for {route_type}-optimisation, returning unsmoothed route."
            }
        else:
            # No route available for this type - skip it
            continue

        # Extract optimisation metrics from route properties
        properties = (
            route_geojson["features"][0].get("properties", {})
            if route_geojson
            else {}
        )
        optimisation_metrics = self._build_optimisation_metrics(
            route_type, properties
        )

        # Build mesh information
        mesh_info = self._build_mesh_info(instance)

        # Build structured route object
        route_obj = {
            "type": route_type,
            "id": str(instance.id),
            "name": f"{data.get('start_name') or 'Start'} to {data.get('end_name') or 'End'} ({route_type})",
            "job": {
                "requestedAt": data["requested"],
                "calculatedAt": data["calculated"],
            },
            "waypoints": {
                "start": {
                    "lat": data["start_lat"],
                    "lon": data["start_lon"],
                    "name": data.get("start_name"),
                },
                "end": {
                    "lat": data["end_lat"],
                    "lon": data["end_lon"],
                    "name": data.get("end_name"),
                },
            },
            "path": route_geojson,
            "unsmoothedPath": unsmoothed_geojson,
            "optimisation": {"metrics": optimisation_metrics},
        }

        if mesh_info:
            route_obj["mesh"] = mesh_info

        # Add any info/warnings
        if info_message:
            route_obj["info"] = info_message
        elif data.get("info"):
            route_obj["info"] = data["info"]

        available_routes.append(route_obj)

    # Return the appropriate format
    if len(available_routes) == 0:
        # No routes available - return error
        result = {
            "type": "error",
            "id": str(instance.id),
            "name": f"{data.get('start_name') or 'Start'} to {data.get('end_name') or 'End'}",
            "job": {
                "requestedAt": data["requested"],
                "calculatedAt": data["calculated"],
            },
            "info": {"error": "No routes available for any optimisation type."},
        }
    elif len(available_routes) == 1:
        # Single route type - return the route directly
        result = available_routes[0]
    else:
        # Multiple route types - return as array
        result = {"routes": available_routes}

    # Add version to all responses
    if isinstance(result, dict):
        result["polarrouteserver-version"] = polarrouteserver_version

    return result