diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index 7e56f856..10f52c98 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -1,6 +1,7 @@ import json from typing import Annotated, Union +from celery import chain from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Prefetch from django.shortcuts import redirect @@ -16,6 +17,7 @@ from c3nav.api.exceptions import API404, APIPermissionDenied, APIRequestValidati from c3nav.api.schema import BaseSchema from c3nav.api.utils import NonEmptyStr from c3nav.mapdata.api.base import api_etag, api_stats, can_access_geometry +from c3nav.mapdata.grid import grid from c3nav.mapdata.models import Source, Theme, Area, Space from c3nav.mapdata.models.geometry.space import ObstacleGroup, Obstacle from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position, LocationGroup @@ -26,7 +28,7 @@ from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableL LocationDisplay, ProjectionPipelineSchema, ProjectionSchema, SlimListableLocationSchema, SlimLocationSchema, all_location_definitions, listable_location_definitions, LegendSchema, LegendItemSchema) -from c3nav.mapdata.schemas.responses import LocationGeometry, WithBoundsSchema +from c3nav.mapdata.schemas.responses import LocationGeometry, WithBoundsSchema, MapSettingsSchema from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request, searchable_locations_for_request, visible_locations_for_request) from c3nav.mapdata.utils.user import can_access_editor @@ -34,6 +36,25 @@ from c3nav.mapdata.utils.user import can_access_editor map_api_router = APIRouter(tags=["map"]) +@map_api_router.get('/settings/', summary="get map settings", + description="get useful/required settings for displaying the map", + response={200: MapSettingsSchema, **auth_responses}) +@api_etag(permissions=False) +def map_settings(request): + initial_bounds = settings.INITIAL_BOUNDS + if not initial_bounds: + initial_bounds = tuple(chain(*Source.max_bounds())) + else: + initial_bounds = (tuple(settings.INITIAL_BOUNDS)[:2], tuple(settings.INITIAL_BOUNDS)[2:]) + + return MapSettingsSchema( + initial_bounds=initial_bounds, + initial_level=settings.INITIAL_LEVEL or None, + grid=grid.serialize().model_dump() if grid else None, + tile_server=settings.TILE_CACHE_SERVER, + ) + + @map_api_router.get('/bounds/', summary="get boundaries", description="get maximum boundaries of everything on the map", response={200: WithBoundsSchema, **auth_responses}) diff --git a/src/c3nav/mapdata/grid.py b/src/c3nav/mapdata/grid.py index 3b7ac1df..8cd449b1 100644 --- a/src/c3nav/mapdata/grid.py +++ b/src/c3nav/mapdata/grid.py @@ -1,9 +1,11 @@ import bisect import string from abc import ABC, abstractmethod -from typing import Optional +from dataclasses import dataclass, field +from typing import Optional, ClassVar, Sequence from django.conf import settings +from ninja import Schema class AbstractGrid(ABC): @@ -18,6 +20,13 @@ class AbstractGrid(ABC): pass +class GridSchema(Schema): + rows: Sequence[float] + cols: Sequence[float] + invert_x: bool + invert_y: bool + + class Grid(AbstractGrid): enabled = True @@ -41,6 +50,14 @@ class Grid(AbstractGrid): else: raise ValueError('column coordinates are not ordered') + def serialize(self) -> GridSchema: + return GridSchema( + rows=self.rows, + cols=self.cols, + invert_x=self.invert_y, + invert_y=self.invert_y, + ) + def get_square_for_point(self, x, y): x = bisect.bisect(self.cols, x) if x <= 0 or x >= len(self.cols): diff --git a/src/c3nav/mapdata/schemas/responses.py b/src/c3nav/mapdata/schemas/responses.py index c5842937..9092929f 100644 --- a/src/c3nav/mapdata/schemas/responses.py +++ b/src/c3nav/mapdata/schemas/responses.py @@ -1,12 +1,36 @@ -from typing import Annotated, Union +from typing import Annotated, Union, Optional from pydantic import Field as APIField from pydantic import PositiveInt from c3nav.api.schema import BaseSchema, GeometrySchema +from c3nav.mapdata.grid import GridSchema from c3nav.mapdata.schemas.model_base import AnyLocationID, BoundsSchema +class MapSettingsSchema(BaseSchema): + """ + various c3nav instance settings + """ + initial_bounds: Optional[BoundsSchema] = APIField( + title="initial boundaries", + description="(left, bottom) to (top, right)", + ) + initial_level: Optional[PositiveInt] = APIField( + title="initial level id", + description="the level id that is intially shown when opening the map", + ) + + grid: Optional[GridSchema] = APIField( + title="grid config", + description="grid configuration, if available", + ) + tile_server: Optional[str] = APIField( + title="tile server base URL", + description="tile server base URL to use, if configured", + ) + + class WithBoundsSchema(BaseSchema): """ Describing a bounding box diff --git a/src/c3nav/site/views.py b/src/c3nav/site/views.py index 5be3fa3b..fa414fd4 100644 --- a/src/c3nav/site/views.py +++ b/src/c3nav/site/views.py @@ -217,12 +217,7 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N } if grid.enabled: - ctx['grid'] = json.dumps({ - 'rows': grid.rows, - 'cols': grid.cols, - 'invert_x': grid.invert_x, - 'invert_y': grid.invert_y, - }, separators=(',', ':'), cls=DjangoJSONEncoder) + ctx['grid'] = json.dumps(grid.serialize().model_dump(), separators=(',', ':'), cls=DjangoJSONEncoder) csrf.get_token(request)