Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/python/app/models/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class RouteBase(SQLModel):
default=None, sa_column=Column(DateTime(timezone=True), nullable=True)
)
expires_at: datetime | None = Field(default=None)
ends_at_warehouse: bool = Field(default=False)


class Route(RouteBase, BaseModel, table=True):
Expand Down Expand Up @@ -69,6 +70,7 @@ class RouteUpdate(SQLModel):
encoded_polyline: str | None = None
polyline_updated_at: datetime | None = None
expires_at: datetime | None = None
ends_at_warehouse: bool | None = None


class RouteWithDateRead(SQLModel):
Expand Down
111 changes: 111 additions & 0 deletions backend/python/app/utilities/routes_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from typing import TYPE_CHECKING

from fastapi import HTTPException
from google.api_core import exceptions as google_exceptions
from google.api_core.client_options import ClientOptions
from google.maps import routing_v2

from app.config import settings

if TYPE_CHECKING:
from app.models.location import Location


async def fetch_route_polyline(
locations: list["Location"],
warehouse_lat: float,
warehouse_lon: float,
ends_at_warehouse: bool,
) -> tuple[str, float]:
"""Fetch encoded polyline from Google Maps Routes API.

Args:
locations: Ordered list of Location objects (route stops)
warehouse_lat: Warehouse latitude
warehouse_lon: Warehouse longitude
ends_at_warehouse: If True, route returns to warehouse

Returns:
Tuple with encoded polyline string and total distance in kilometers

Raises:
HTTPException: If API request fails
ValueError: If locations list is empty or API key not configured
"""
if not locations:
raise ValueError("Locations list cannot be empty")

if not settings.google_maps_api_key:
raise ValueError("Google Maps API key is not configured in settings")

# Build waypoints
origin = routing_v2.Waypoint(
location=routing_v2.Location(
lat_lng={"latitude": warehouse_lat, "longitude": warehouse_lon}
)
)

intermediates = [
routing_v2.Waypoint(
location=routing_v2.Location(
lat_lng={"latitude": loc.latitude, "longitude": loc.longitude}
)
)
for loc in locations
]

if ends_at_warehouse:
destination = origin
waypoints_to_use = intermediates
else:
if len(intermediates) > 1:
waypoints_to_use = intermediates[:-1]
destination = intermediates[-1]
else:
waypoints_to_use = None
destination = intermediates[0]

# Build request
request = routing_v2.ComputeRoutesRequest(
origin=origin,
destination=destination,
intermediates=waypoints_to_use,
travel_mode=routing_v2.RouteTravelMode.DRIVE,
routing_preference=routing_v2.RoutingPreference.TRAFFIC_AWARE,
)

try:
# Create client with API key
options = ClientOptions(api_key=settings.google_maps_api_key)
client = routing_v2.RoutesAsyncClient(client_options=options)

response = await client.compute_routes(
request=request,
metadata=[
(
"x-goog-fieldmask",
"routes.polyline.encodedPolyline,routes.distanceMeters",
)
],
)

if not response.routes:
raise HTTPException(
status_code=500,
detail="Google Maps API returned no routes",
)

route = response.routes[0]
polyline = route.polyline.encoded_polyline
distance_km = route.distance_meters / 1000.0

return polyline, distance_km

except google_exceptions.GoogleAPICallError as e:
raise HTTPException(
status_code=503, detail=f"Google Maps API error: {e!s}"
) from e
except google_exceptions.RetryError as e:
raise HTTPException(status_code=504, detail="Request timed out") from e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error: {e!s}") from e
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""added ends_at_warehouse to route model

Revision ID: eb010a6ed5ad
Revises: ba76119b3e4c
Create Date: 2025-12-01 00:31:47.827096

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'eb010a6ed5ad'
down_revision = 'ba76119b3e4c'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('routes', sa.Column('ends_at_warehouse', sa.Boolean(), nullable=False, server_default='false'))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('routes', 'ends_at_warehouse')
# ### end Alembic commands ###
1 change: 1 addition & 0 deletions backend/python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ google-api-core>=2.0.0
google-cloud-core>=1.6.0
google-cloud-firestore>=2.1.0
google-cloud-storage>=1.37.1
google-maps-routing>=0.1.0
googleapis-common-protos>=1.70.0
firebase-admin>=6.0.0

Expand Down