from django.urls import Resolver404, resolve from django.utils.translation import gettext_lazy as _ from ninja import Router as APIRouter from shapely.geometry.geo import mapping from c3nav.api.auth import APIKeyAuth, auth_permission_responses from c3nav.api.exceptions import API404 from c3nav.editor.api.base import api_etag_with_update_cache_key from c3nav.editor.api.geometries import get_level_geometries_result, get_space_geometries_result from c3nav.editor.api.schemas import EditorGeometriesElemSchema, EditorID, GeometryStylesSchema, UpdateCacheKey, \ EditorBeaconsLookup from c3nav.editor.views.base import editor_etag_func from c3nav.mapdata.api.base import api_etag from c3nav.mapdata.models import Source from c3nav.mapdata.schemas.responses import WithBoundsSchema from c3nav.mesh.utils import get_nodes_and_ranging_beacons editor_api_router = APIRouter(tags=["editor"], auth=APIKeyAuth(permissions={"editor_access"})) @editor_api_router.get('/bounds/', summary="boundaries", description="get maximum boundaries of everything on the map", response={200: WithBoundsSchema, **auth_permission_responses}, openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]}) @api_etag() def bounds(request): return { "bounds": Source.max_bounds(), } @editor_api_router.get('/geometrystyles/', summary="geometry styles", description="get the default colors for each geometry type", response={200: GeometryStylesSchema, **auth_permission_responses}, openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]}) @api_etag(permissions=False) def geometrystyles(request): return { 'building': '#aaaaaa', 'space': '#eeeeee', 'hole': 'rgba(255, 0, 0, 0.3)', 'door': '#ffffff', 'area': '#55aaff', 'stair': '#a000a0', 'ramp': 'rgba(160, 0, 160, 0.2)', 'obstacle': '#999999', 'lineobstacle': '#999999', 'column': 'rgba(0, 0, 50, 0.3)', 'poi': '#4488cc', 'shadow': '#000000', 'graphnode': '#009900', 'graphedge': '#00CC00', 'altitudemarker': '#0000FF', 'beaconmeasurement': '#DDDD00', 'rangingbeacon': '#CC00CC', } @editor_api_router.get('/geometries/space/{space_id}/', summary="space geometries", description="get the geometries to display on the editor map for a space", response={200: list[EditorGeometriesElemSchema], **API404.dict(), **auth_permission_responses}, openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]}) @api_etag_with_update_cache_key(etag_func=editor_etag_func) def space_geometries(request, space_id: EditorID, update_cache_key: UpdateCacheKey = None, **kwargs): # newapi_etag_with_update_cache_key does the following, don't let it confuse you: # - update_cache_key becomes the actual update_cache_key, not the one supplied be the user # - kwargs has "update_cache_key_match", which is true if update_cache_key matches the one supplied be the user # this is done so the api etag is correctly generated, as it takes the function arguments into account return get_space_geometries_result( request, space_id=space_id, update_cache_key=update_cache_key, update_cache_key_match=kwargs["update_cache_key_match"] ) @editor_api_router.get('/geometries/level/{level_id}/', summary="level geometries", description="get the geometries to display on the editor map for a space", response={200: list[EditorGeometriesElemSchema], **API404.dict(), **auth_permission_responses}, openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]}) @api_etag_with_update_cache_key(etag_func=editor_etag_func) def level_geometries(request, level_id: EditorID, update_cache_key: UpdateCacheKey = None, **kwargs): # newapi_etag_with_update_cache_key does the following, don't let it confuse you: # - update_cache_key becomes the actual update_cache_key, not the one supplied be the user # - kwargs has "update_cache_key_match", which is true if update_cache_key matches the one supplied be the user # this is done so the api etag is correctly generated, as it takes the function arguments into account return get_level_geometries_result( request, level_id=level_id, update_cache_key=update_cache_key, update_cache_key_match=kwargs["update_cache_key_match"] ) # todo: need a way to pass the changeset if it's not a session API key def resolve_editor_path_api(request, path): resolved = None if path: try: resolved = resolve('/editor/'+path+'/') except Resolver404: pass if not resolved: try: resolved = resolve('/editor/'+path) except Resolver404: pass request.sub_resolver_match = resolved return resolved @editor_api_router.get('/as_api/{path:path}', summary="raw editor access", response={200: dict, **API404.dict(), **auth_permission_responses}, openapi_extra={"security": [{"APIKeyAuth": ["editor_access"]}]}) @api_etag() # todo: correct? def get_view_as_api(request, path: str): """ get editor views rendered as JSON instead of HTML. `path` is the path after /editor/. this is a mess. good luck. if you actually want to use this, poke us so we might add better documentation. """ resolved = resolve_editor_path_api(request, path) if not resolved: raise API404(_('No matching editor view endpoint found.')) if not getattr(resolved.func, 'api_hybrid', False): raise API404(_('Matching editor view point does not provide an API.')) response = resolved.func(request, api=True, *resolved.args, **resolved.kwargs) return response @editor_api_router.post('/as_api/{path:path}', summary="raw editor access", response={200: dict, **API404.dict(), **auth_permission_responses}, openapi_extra={"security": [{"APIKeyAuth": ["editor_access", "write"]}]}) @api_etag() # todo: correct? def post_view_as_api(request, path: str): """ get editor views rendered as JSON instead of HTML. `path` is the path after /editor/. this is a mess. good luck. if you actually want to use this, poke us so we might add better documentation. """ raise NotImplementedError @editor_api_router.get('/beacons-lookup/', summary="get beacon coordinates", description="get xyz coordinates for all known positioning beacons", response={200: EditorBeaconsLookup, **auth_permission_responses}, openapi_extra={"security": [{"APIKeyAuth": ["editor_access", "write"]}]}) def beacons_lookup(request): # todo: update with more details? todo permission? from c3nav.mesh.messages import MeshMessageType calculated = get_nodes_and_ranging_beacons() wifi_beacons = {} ibeacons = {} for beacon in calculated.beacons.values(): node = calculated.nodes_for_beacons.get(beacon.id, None) beacon_data = { "name": node.name if node else ("Beacon #%d" % beacon.pk), "point": mapping(beacon.geometry), } if beacon.wifi_bssid: wifi_beacons[beacon.wifi_bssid] = beacon_data if beacon.ibeacon_uuid and beacon.ibeacon_major is not None and beacon.ibeacon_minor is not None: ibeacons.setdefault( str(beacon.ibeacon_uuid), {} ).setdefault( beacon.ibeacon_major, {} )[beacon.ibeacon_minor] = beacon_data for node in calculated.nodes.values(): beacon_data = { "name": node.name, "point": None, } ibeacon_msg = node.last_messages[MeshMessageType.CONFIG_IBEACON] if ibeacon_msg: ibeacons.setdefault( str(ibeacon_msg.parsed.content.uuid), {} ).setdefault( ibeacon_msg.parsed.content.major, {} ).setdefault( ibeacon_msg.parsed.content.minor, beacon_data ) node_msg = node.last_messages[MeshMessageType.CONFIG_NODE] if node_msg: wifi_beacons.setdefault(node.address, beacon_data) return EditorBeaconsLookup( wifi_beacons=wifi_beacons, ibeacons=ibeacons, ).model_dump(mode="json")