2023-11-23 21:11:31 +01:00
|
|
|
import json
|
2024-12-29 23:55:58 +01:00
|
|
|
import math
|
2024-12-26 01:26:24 +01:00
|
|
|
from typing import Annotated, Union, Optional
|
2023-11-23 21:11:31 +01:00
|
|
|
|
2024-12-03 15:05:53 +01:00
|
|
|
from celery import chain
|
2024-12-29 20:11:34 +01:00
|
|
|
from django.core.cache import cache
|
2023-11-23 21:11:31 +01:00
|
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
2024-12-29 20:11:34 +01:00
|
|
|
from django.db import transaction
|
2024-12-29 20:01:53 +01:00
|
|
|
from django.db.models import Prefetch, Q
|
2023-11-23 21:11:31 +01:00
|
|
|
from django.shortcuts import redirect
|
2023-11-24 15:42:48 +01:00
|
|
|
from django.utils import timezone
|
2023-11-23 16:37:25 +01:00
|
|
|
from ninja import Query
|
2023-11-19 15:34:08 +01:00
|
|
|
from ninja import Router as APIRouter
|
2023-11-23 16:37:25 +01:00
|
|
|
from pydantic import Field as APIField
|
2023-12-03 23:27:20 +01:00
|
|
|
from pydantic import PositiveInt
|
2023-11-19 15:34:08 +01:00
|
|
|
|
2024-08-13 21:17:36 +02:00
|
|
|
from c3nav import settings
|
2023-12-03 21:55:08 +01:00
|
|
|
from c3nav.api.auth import auth_permission_responses, auth_responses, validate_responses
|
2023-11-24 15:42:48 +01:00
|
|
|
from c3nav.api.exceptions import API404, APIPermissionDenied, APIRequestValidationFailed
|
2023-12-11 20:49:50 +01:00
|
|
|
from c3nav.api.schema import BaseSchema
|
2023-11-23 22:44:09 +01:00
|
|
|
from c3nav.api.utils import NonEmptyStr
|
2023-12-04 22:27:01 +01:00
|
|
|
from c3nav.mapdata.api.base import api_etag, api_stats, can_access_geometry
|
2024-12-03 15:05:53 +01:00
|
|
|
from c3nav.mapdata.grid import grid
|
2024-09-06 15:07:54 +02:00
|
|
|
from c3nav.mapdata.models import Source, Theme, Area, Space
|
2024-12-29 19:44:36 +01:00
|
|
|
from c3nav.mapdata.models.geometry.space import ObstacleGroup, Obstacle, RangingBeacon
|
2024-12-25 17:01:30 +01:00
|
|
|
from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Position, LocationGroup, LoadGroup
|
2024-12-24 22:58:26 +01:00
|
|
|
from c3nav.mapdata.quests.base import QuestSchema, get_all_quests_for_request
|
2024-09-06 15:07:54 +02:00
|
|
|
from c3nav.mapdata.render.theme import ColorManager
|
2023-11-23 18:10:31 +01:00
|
|
|
from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter
|
2023-12-07 02:15:32 +01:00
|
|
|
from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID
|
2023-11-24 01:05:38 +01:00
|
|
|
from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableLocationSchema, FullLocationSchema,
|
2024-08-13 21:17:36 +02:00
|
|
|
LocationDisplay, ProjectionPipelineSchema, ProjectionSchema,
|
|
|
|
SlimListableLocationSchema, SlimLocationSchema, all_location_definitions,
|
2024-09-06 15:07:54 +02:00
|
|
|
listable_location_definitions, LegendSchema, LegendItemSchema)
|
2024-12-03 15:05:53 +01:00
|
|
|
from c3nav.mapdata.schemas.responses import LocationGeometry, WithBoundsSchema, MapSettingsSchema
|
2024-12-29 20:01:53 +01:00
|
|
|
from c3nav.mapdata.utils.geometry import unwrap_geom
|
2023-11-23 22:44:09 +01:00
|
|
|
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)
|
2023-11-23 21:11:31 +01:00
|
|
|
from c3nav.mapdata.utils.user import can_access_editor
|
2023-11-19 15:34:08 +01:00
|
|
|
|
|
|
|
map_api_router = APIRouter(tags=["map"])
|
|
|
|
|
|
|
|
|
2024-12-03 15:05:53 +01:00
|
|
|
@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,
|
2024-12-04 12:20:27 +01:00
|
|
|
grid=grid if grid else None,
|
2024-12-03 15:05:53 +01:00
|
|
|
tile_server=settings.TILE_CACHE_SERVER,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:04:23 +01:00
|
|
|
@map_api_router.get('/bounds/', summary="get boundaries",
|
|
|
|
description="get maximum boundaries of everything on the map",
|
2023-12-04 18:58:49 +01:00
|
|
|
response={200: WithBoundsSchema, **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_etag(permissions=False)
|
2023-11-19 15:34:08 +01:00
|
|
|
def bounds(request):
|
|
|
|
return {
|
|
|
|
"bounds": Source.max_bounds(),
|
|
|
|
}
|
2023-11-23 16:37:25 +01:00
|
|
|
|
|
|
|
|
2023-12-11 20:49:50 +01:00
|
|
|
class LocationEndpointParameters(BaseSchema):
|
2023-11-23 16:37:25 +01:00
|
|
|
searchable: bool = APIField(
|
|
|
|
False,
|
|
|
|
title='only list searchable locations',
|
|
|
|
description='if set, only searchable locations will be listed'
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-11-23 18:10:31 +01:00
|
|
|
class LocationListFilters(BySearchableFilter, RemoveGeometryFilter):
|
|
|
|
pass
|
|
|
|
|
2023-11-23 16:37:25 +01:00
|
|
|
|
2024-12-03 18:42:33 +01:00
|
|
|
def _location_list(request, filters: LocationListFilters):
|
2023-11-24 16:33:57 +01:00
|
|
|
if filters.searchable:
|
|
|
|
locations = searchable_locations_for_request(request)
|
|
|
|
else:
|
|
|
|
locations = visible_locations_for_request(request).values()
|
|
|
|
|
2024-12-03 18:42:33 +01:00
|
|
|
for location in locations:
|
|
|
|
if not filters.geometry or not can_access_geometry(request, location):
|
|
|
|
location._hide_geometry = True
|
|
|
|
|
|
|
|
return locations
|
2023-11-23 18:10:31 +01:00
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/', summary="list locations (slim)",
|
2023-12-04 18:58:49 +01:00
|
|
|
description=("Get locations (with most important attributes set)\n\n"
|
|
|
|
"Possible location types:\n"+listable_location_definitions),
|
2023-12-03 19:35:19 +01:00
|
|
|
response={200: list[SlimListableLocationSchema], **validate_responses, **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-23 18:10:31 +01:00
|
|
|
def location_list(request, filters: Query[LocationListFilters]):
|
2024-12-03 18:42:33 +01:00
|
|
|
return _location_list(request, filters=filters)
|
2023-11-23 18:10:31 +01:00
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/full/', summary="list locations (full)",
|
2023-12-04 18:58:49 +01:00
|
|
|
description=("Get locations (with all attributes set)\n\n"
|
|
|
|
"Possible location types:\n"+listable_location_definitions),
|
2023-12-03 19:35:19 +01:00
|
|
|
response={200: list[FullListableLocationSchema], **validate_responses, **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-23 18:10:31 +01:00
|
|
|
def location_list_full(request, filters: Query[LocationListFilters]):
|
2024-12-03 18:42:33 +01:00
|
|
|
return _location_list(request, filters=filters)
|
2023-11-23 21:11:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _location_retrieve(request, location, detailed: bool, geometry: bool, show_redirects: bool):
|
|
|
|
if location is None:
|
2023-11-24 15:42:48 +01:00
|
|
|
raise API404()
|
2023-11-23 21:11:31 +01:00
|
|
|
|
|
|
|
if isinstance(location, LocationRedirect):
|
|
|
|
if not show_redirects:
|
|
|
|
return redirect('../' + str(location.target.slug)) # todo: use reverse, make pk and slug both work
|
|
|
|
|
2023-11-24 17:24:07 +01:00
|
|
|
if isinstance(location, (DynamicLocation, Position)):
|
|
|
|
request._target_etag = None
|
|
|
|
request._target_cache_key = None
|
|
|
|
|
2024-12-03 18:42:33 +01:00
|
|
|
if not geometry or not can_access_geometry(request, location):
|
|
|
|
location._hide_geometry = True
|
|
|
|
|
|
|
|
return location
|
2023-11-23 21:11:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _location_display(request, location):
|
|
|
|
if location is None:
|
2023-11-24 15:42:48 +01:00
|
|
|
raise API404()
|
2023-11-23 21:11:31 +01:00
|
|
|
|
|
|
|
if isinstance(location, LocationRedirect):
|
|
|
|
return redirect('../' + str(location.target.slug) + '/details/') # todo: use reverse, make pk+slug work
|
|
|
|
|
|
|
|
result = location.details_display(
|
2023-12-04 22:27:01 +01:00
|
|
|
detailed_geometry=can_access_geometry(request, location),
|
2023-11-23 21:11:31 +01:00
|
|
|
editor_url=can_access_editor(request)
|
|
|
|
)
|
|
|
|
return json.loads(json.dumps(result, cls=DjangoJSONEncoder)) # todo: wtf?? well we need to get rid of lazy strings
|
|
|
|
|
|
|
|
|
2023-11-24 01:23:07 +01:00
|
|
|
def _location_geometry(request, location):
|
|
|
|
# todo: cache, visibility, etc…
|
|
|
|
|
|
|
|
if location is None:
|
2023-11-24 15:42:48 +01:00
|
|
|
raise API404()
|
2023-11-24 01:23:07 +01:00
|
|
|
|
|
|
|
if isinstance(location, LocationRedirect):
|
|
|
|
return redirect('../' + str(location.target.slug) + '/geometry/') # todo: use reverse, make pk+slug work
|
|
|
|
|
|
|
|
return LocationGeometry(
|
|
|
|
id=location.pk,
|
|
|
|
level=getattr(location, 'level_id', None),
|
|
|
|
geometry=location.get_geometry(
|
2023-12-04 22:27:01 +01:00
|
|
|
detailed_geometry=can_access_geometry(request, location)
|
2023-11-24 01:23:07 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-11 20:49:50 +01:00
|
|
|
class ShowRedirects(BaseSchema):
|
2023-11-23 21:11:31 +01:00
|
|
|
show_redirects: bool = APIField(
|
|
|
|
False,
|
|
|
|
name="show redirects",
|
|
|
|
description="whether to show redirects instead of sending a redirect response",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/{location_id}/', summary="location by ID (slim)",
|
2023-12-04 18:58:49 +01:00
|
|
|
description=("Get locations by ID (with all attributes set)\n\n"
|
|
|
|
"Possible location types:\n"+all_location_definitions),
|
2023-12-03 19:35:19 +01:00
|
|
|
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('location_get')
|
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-24 01:05:38 +01:00
|
|
|
def location_by_id(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
|
2023-11-23 22:44:09 +01:00
|
|
|
redirects: Query[ShowRedirects]):
|
2023-11-23 21:11:31 +01:00
|
|
|
return _location_retrieve(
|
|
|
|
request,
|
|
|
|
get_location_by_id_for_request(location_id, request),
|
|
|
|
detailed=False, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/{location_id}/full/', summary="location by ID (full)",
|
2023-12-04 18:58:49 +01:00
|
|
|
description=("Get location by ID (with all attributes set)\n\n"
|
|
|
|
"Possible location types:\n"+all_location_definitions),
|
2023-12-03 19:35:19 +01:00
|
|
|
response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('location_get')
|
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-24 01:05:38 +01:00
|
|
|
def location_by_id_full(request, location_id: AnyLocationID, filters: Query[RemoveGeometryFilter],
|
2023-11-23 22:44:09 +01:00
|
|
|
redirects: Query[ShowRedirects]):
|
2023-11-23 21:11:31 +01:00
|
|
|
return _location_retrieve(
|
|
|
|
request,
|
|
|
|
get_location_by_id_for_request(location_id, request),
|
|
|
|
detailed=True, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/{location_id}/display/', summary="location display by ID",
|
|
|
|
description="Get location display information by ID",
|
|
|
|
response={200: LocationDisplay, **API404.dict(), **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('location_display')
|
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-24 01:05:38 +01:00
|
|
|
def location_by_id_display(request, location_id: AnyLocationID):
|
2023-11-23 21:11:31 +01:00
|
|
|
return _location_display(
|
|
|
|
request,
|
|
|
|
get_location_by_id_for_request(location_id, request),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/{location_id}/geometry/', summary="location geometry by id",
|
|
|
|
description="Get location geometry (if available) by ID",
|
|
|
|
response={200: LocationGeometry, **API404.dict(), **auth_responses})
|
2024-03-30 21:47:35 +01:00
|
|
|
@api_stats('location_geometry')
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-24 01:23:07 +01:00
|
|
|
def location_by_id_geometry(request, location_id: AnyLocationID):
|
|
|
|
return _location_geometry(
|
|
|
|
request,
|
|
|
|
get_location_by_id_for_request(location_id, request),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/by-slug/{location_slug}/', summary="location by slug (slim)",
|
2023-12-04 18:58:49 +01:00
|
|
|
description=("Get location by slug (with most important attributes set)\n\n"
|
|
|
|
"Possible location types:\n"+all_location_definitions),
|
2023-12-03 19:35:19 +01:00
|
|
|
response={200: SlimLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('location_get')
|
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-23 22:44:09 +01:00
|
|
|
def location_by_slug(request, location_slug: NonEmptyStr, filters: Query[RemoveGeometryFilter],
|
|
|
|
redirects: Query[ShowRedirects]):
|
|
|
|
return _location_retrieve(
|
|
|
|
request,
|
|
|
|
get_location_by_slug_for_request(location_slug, request),
|
|
|
|
detailed=False, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/by-slug/{location_slug}/full/', summary="location by slug (full)",
|
2023-12-04 18:58:49 +01:00
|
|
|
description=("Get location by slug (with all attributes set)\n\n"
|
|
|
|
"Possible location types:\n"+all_location_definitions),
|
2023-12-03 19:35:19 +01:00
|
|
|
response={200: FullLocationSchema, **API404.dict(), **validate_responses, **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('location_get')
|
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-23 22:44:09 +01:00
|
|
|
def location_by_slug_full(request, location_slug: NonEmptyStr, filters: Query[RemoveGeometryFilter],
|
|
|
|
redirects: Query[ShowRedirects]):
|
|
|
|
return _location_retrieve(
|
|
|
|
request,
|
|
|
|
get_location_by_slug_for_request(location_slug, request),
|
|
|
|
detailed=True, geometry=filters.geometry, show_redirects=redirects.show_redirects,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/by-slug/{location_slug}/display/', summary="location display by slug",
|
|
|
|
description="Get location display information by slug",
|
|
|
|
response={200: LocationDisplay, **API404.dict(), **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('location_display')
|
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-23 22:44:09 +01:00
|
|
|
def location_by_slug_display(request, location_slug: NonEmptyStr):
|
|
|
|
return _location_display(
|
|
|
|
request,
|
|
|
|
get_location_by_slug_for_request(location_slug, request),
|
|
|
|
)
|
2023-11-24 01:05:38 +01:00
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/locations/by-slug/{location_slug}/geometry/', summary="location geometry by slug",
|
|
|
|
description="Get location geometry (if available) by slug",
|
|
|
|
response={200: LocationGeometry, **API404.dict(), **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('location_geometry')
|
|
|
|
@api_etag(base_mapdata=True)
|
2023-11-24 01:23:07 +01:00
|
|
|
def location_by_slug_geometry(request, location_slug: NonEmptyStr):
|
|
|
|
return _location_geometry(
|
|
|
|
request,
|
|
|
|
get_location_by_slug_for_request(location_slug, request),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-12-29 18:54:46 +01:00
|
|
|
@map_api_router.get('/positions/my/', summary="all moving position coordinates",
|
|
|
|
description="get current coordinates of all moving positions owned be the current users",
|
|
|
|
response={200: list[AnyPositionStatusSchema], **API404.dict(), **auth_responses})
|
|
|
|
@api_stats('get_positions')
|
|
|
|
def get_my_positions(request):
|
|
|
|
# no caching for obvious reasons!
|
|
|
|
return [
|
|
|
|
position.serialize_position(request=request)
|
|
|
|
for position in Position.objects.filter(owner=request.user)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2023-12-03 19:35:19 +01:00
|
|
|
@map_api_router.get('/positions/{position_id}/', summary="moving position coordinates",
|
|
|
|
description="get current coordinates of a moving position / dynamic location",
|
|
|
|
response={200: AnyPositionStatusSchema, **API404.dict(), **auth_responses})
|
2023-12-03 21:55:08 +01:00
|
|
|
@api_stats('get_position')
|
2023-11-24 17:37:33 +01:00
|
|
|
def get_position_by_id(request, position_id: AnyPositionID):
|
2023-11-24 14:35:48 +01:00
|
|
|
# no caching for obvious reasons!
|
2023-11-24 01:05:38 +01:00
|
|
|
location = None
|
|
|
|
if isinstance(position_id, int) or position_id.isdigit():
|
|
|
|
location = get_location_by_id_for_request(position_id, request)
|
|
|
|
if not isinstance(location, DynamicLocation):
|
2023-11-24 15:42:48 +01:00
|
|
|
raise API404()
|
2024-12-19 12:26:18 +01:00
|
|
|
if location is None and position_id.startswith('m:'):
|
2023-11-24 01:05:38 +01:00
|
|
|
try:
|
|
|
|
location = Position.objects.get(secret=position_id[2:])
|
|
|
|
except Position.DoesNotExist:
|
2023-11-24 15:42:48 +01:00
|
|
|
raise API404()
|
2024-09-18 20:35:18 +02:00
|
|
|
|
|
|
|
return location.serialize_position(request=request)
|
2023-11-24 15:42:48 +01:00
|
|
|
|
2023-12-07 02:15:32 +01:00
|
|
|
|
2023-12-11 20:49:50 +01:00
|
|
|
class UpdatePositionSchema(BaseSchema):
|
2023-12-03 23:27:20 +01:00
|
|
|
coordinates_id: Union[
|
|
|
|
Annotated[CustomLocationID, APIField(title="set coordinates")],
|
|
|
|
Annotated[None, APIField(title="unset coordinates")],
|
|
|
|
] = APIField(
|
|
|
|
description="coordinates to set the location to or null to unset it"
|
2023-11-24 15:42:48 +01:00
|
|
|
)
|
2023-12-03 23:27:20 +01:00
|
|
|
timeout: Union[
|
|
|
|
Annotated[PositiveInt, APIField(title="new timeout")],
|
|
|
|
Annotated[None, APIField(title="don't change")],
|
|
|
|
] = APIField(
|
2023-11-24 15:42:48 +01:00
|
|
|
None,
|
2023-12-03 23:27:20 +01:00
|
|
|
title="timeout",
|
2023-11-24 15:42:48 +01:00
|
|
|
description="timeout for this new location in seconds, or None if not to change it",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-11-24 17:37:33 +01:00
|
|
|
@map_api_router.put('/positions/{position_id}/', url_name="position-update",
|
2023-12-03 19:35:19 +01:00
|
|
|
summary="set moving position",
|
|
|
|
description="only the string ID for the position secret must be used",
|
|
|
|
response={200: AnyPositionStatusSchema, **API404.dict(), **auth_permission_responses})
|
2023-11-24 17:37:33 +01:00
|
|
|
def set_position(request, position_id: AnyPositionID, update: UpdatePositionSchema):
|
2023-11-24 15:42:48 +01:00
|
|
|
# todo: may an API key do this?
|
2024-12-19 12:26:18 +01:00
|
|
|
if not isinstance(position_id, str) or not position_id.startswith('m:'):
|
2023-11-24 15:42:48 +01:00
|
|
|
raise API404()
|
|
|
|
try:
|
2023-12-28 15:27:43 +01:00
|
|
|
location = Position.objects.get(secret=position_id[2:])
|
2023-11-24 15:42:48 +01:00
|
|
|
except Position.DoesNotExist:
|
|
|
|
raise API404()
|
|
|
|
if location.owner != request.user:
|
|
|
|
raise APIPermissionDenied()
|
|
|
|
|
|
|
|
coordinates = get_location_by_id_for_request(update.coordinates_id, request)
|
|
|
|
if coordinates is None:
|
|
|
|
raise APIRequestValidationFailed('Cant resolve coordinates.')
|
|
|
|
|
|
|
|
location.coordinates_id = update.coordinates_id
|
2024-12-29 16:14:55 +01:00
|
|
|
location.timeout = update.timeout or 0
|
2023-11-24 15:42:48 +01:00
|
|
|
location.last_coordinates_update = timezone.now()
|
|
|
|
location.save()
|
|
|
|
|
2024-09-18 20:35:18 +02:00
|
|
|
return location.serialize_position(request=request)
|
2024-08-13 21:17:36 +02:00
|
|
|
|
|
|
|
|
|
|
|
@map_api_router.get('/projection/', summary='get proj4 string',
|
|
|
|
description="get proj4 string for converting WGS84 coordinates to c3nva coordinates",
|
|
|
|
response={200: Union[ProjectionSchema, ProjectionPipelineSchema], **auth_responses})
|
|
|
|
def get_projection(request):
|
|
|
|
obj = {
|
|
|
|
"pipeline": settings.PROJECTION_TRANSFORMER_STRING
|
|
|
|
}
|
|
|
|
if True:
|
|
|
|
obj.update({
|
|
|
|
'proj4': settings.PROJECTION_PROJ4,
|
|
|
|
'zero_point': settings.PROJECTION_ZERO_POINT,
|
|
|
|
'rotation': settings.PROJECTION_ROTATION,
|
|
|
|
'rotation_matrix': settings.PROJECTION_ROTATION_MATRIX,
|
|
|
|
})
|
|
|
|
return obj
|
2024-09-06 15:07:54 +02:00
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
Legend
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@map_api_router.get('/legend/{theme_id}/', summary="get legend",
|
|
|
|
description="Get legend / color key fo theme",
|
|
|
|
response={200: LegendSchema, **API404.dict(), **auth_responses})
|
|
|
|
@api_etag(permissions=True)
|
|
|
|
def legend_for_theme(request, theme_id: int):
|
|
|
|
try:
|
|
|
|
manager = ColorManager.for_theme(theme_id or None)
|
|
|
|
except Theme.DoesNotExist:
|
|
|
|
raise API404()
|
|
|
|
locationgroups = LocationGroup.qs_for_request(request).filter(in_legend=True).prefetch_related(
|
|
|
|
Prefetch('areas', Area.qs_for_request(request))
|
|
|
|
).prefetch_related(
|
|
|
|
Prefetch('spaces', Space.qs_for_request(request))
|
|
|
|
)
|
|
|
|
obstaclegroups = ObstacleGroup.objects.filter(
|
2024-09-06 15:09:32 +02:00
|
|
|
in_legend=True,
|
2024-09-06 15:07:54 +02:00
|
|
|
pk__in=set(Obstacle.qs_for_request(request).filter(group__isnull=False).values_list('group', flat=True)),
|
|
|
|
)
|
|
|
|
return LegendSchema(
|
|
|
|
base=[],
|
|
|
|
groups=[item for item in (LegendItemSchema(title=group.title,
|
|
|
|
fill=manager.locationgroup_fill_color(group),
|
|
|
|
border=manager.locationgroup_border_color(group))
|
|
|
|
for group in locationgroups if group.areas.all() or group.spaces.all())
|
|
|
|
if item.fill or item.border],
|
|
|
|
obstacles=[item for item in (LegendItemSchema(title=group.title,
|
|
|
|
fill=manager.obstaclegroup_fill_color(group),
|
|
|
|
border=manager.obstaclegroup_border_color(group))
|
|
|
|
for group in obstaclegroups)
|
|
|
|
if item.fill or item.border],
|
|
|
|
)
|
2024-12-24 18:50:53 +01:00
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
Quests
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2024-12-26 01:26:24 +01:00
|
|
|
class QuestsFilter(BaseSchema):
|
|
|
|
quest_type: Optional[str] = APIField(
|
|
|
|
None,
|
|
|
|
title="only show these quest types",
|
|
|
|
description="multiple quest types can be comma-separated"
|
|
|
|
)
|
|
|
|
level: Optional[PositiveInt] = APIField(
|
|
|
|
None,
|
|
|
|
title="only show quests for this level",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-12-24 18:50:53 +01:00
|
|
|
@map_api_router.get('/quests/', summary="get open quests",
|
|
|
|
response={200: list[QuestSchema], **auth_responses})
|
2024-12-24 18:57:34 +01:00
|
|
|
@api_etag(permissions=True, quests=True)
|
2024-12-26 01:26:24 +01:00
|
|
|
def list_quests(request, filters: Query[QuestsFilter]):
|
2024-12-26 02:28:38 +01:00
|
|
|
quest_types = filters.quest_type.split(',') if filters.quest_type else None
|
|
|
|
quests = get_all_quests_for_request(request, quest_types)
|
|
|
|
|
2024-12-26 01:26:24 +01:00
|
|
|
if filters.level:
|
|
|
|
quests = [quest for quest in quests if quest.level_id == filters.level]
|
|
|
|
return quests
|
2024-12-25 17:01:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
Room load
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@map_api_router.get('/load/', summary="get load group loads",
|
|
|
|
response={200: dict[PositiveInt, float], **auth_responses})
|
|
|
|
def get_load(request):
|
2024-12-29 20:11:34 +01:00
|
|
|
result = cache.get('mapdata:get_load', None)
|
|
|
|
if result is not None:
|
|
|
|
return result
|
|
|
|
|
2024-12-29 22:17:49 +01:00
|
|
|
if not cache.get('mapdata:load_is_recent', False):
|
|
|
|
return {}
|
|
|
|
|
2024-12-29 20:01:53 +01:00
|
|
|
load_groups = {g.pk: g for g in LoadGroup.objects.all()}
|
|
|
|
|
|
|
|
# per group
|
|
|
|
max_values: dict[int, int] = {pk: 0 for pk in load_groups.keys()}
|
|
|
|
current_values: dict[int, int] = {pk: 0 for pk in load_groups.keys()}
|
|
|
|
|
|
|
|
beacons_by_space = {}
|
|
|
|
for beacon in RangingBeacon.objects.filter(max_observed_num_clients__gt=0):
|
|
|
|
beacons_by_space.setdefault(beacon.space_id, {})[beacon.pk] = beacon
|
|
|
|
|
|
|
|
locationgroups_contribute_to = dict(
|
2024-12-29 21:41:16 +01:00
|
|
|
LocationGroup.objects.filter(load_group_contribute__isnull=False).values_list("pk", "load_group_contribute")
|
2024-12-29 20:01:53 +01:00
|
|
|
)
|
2024-12-29 21:41:16 +01:00
|
|
|
for area in Area.objects.filter((Q(load_group_contribute__isnull=False)
|
2024-12-29 20:01:53 +01:00
|
|
|
| Q(groups__in=locationgroups_contribute_to.keys()))).prefetch_related("groups"):
|
|
|
|
contribute_to = set()
|
2024-12-29 21:41:16 +01:00
|
|
|
if area.load_group_contribute_id:
|
|
|
|
contribute_to.add(area.load_group_contribute_id)
|
2024-12-29 20:01:53 +01:00
|
|
|
for group in area.groups.all():
|
2024-12-29 21:41:16 +01:00
|
|
|
if group.load_group_contribute_id:
|
|
|
|
contribute_to.add(group.load_group_contribute_id)
|
2024-12-29 20:01:53 +01:00
|
|
|
for beacon in beacons_by_space.get(area.space_id, {}).values():
|
|
|
|
if area.geometry.intersects(unwrap_geom(beacon.geometry)):
|
|
|
|
for load_group_id in contribute_to:
|
|
|
|
max_values[load_group_id] += beacon.max_observed_num_clients
|
|
|
|
current_values[load_group_id] += beacon.num_clients
|
|
|
|
|
2024-12-29 21:41:16 +01:00
|
|
|
for space in Space.objects.filter((Q(load_group_contribute__isnull=False)
|
2024-12-29 20:01:53 +01:00
|
|
|
| Q(groups__in=locationgroups_contribute_to.keys()))).prefetch_related("groups"):
|
|
|
|
contribute_to = set()
|
2024-12-29 21:41:16 +01:00
|
|
|
if space.load_group_contribute_id:
|
|
|
|
contribute_to.add(space.load_group_contribute_id)
|
2024-12-29 20:01:53 +01:00
|
|
|
for group in space.groups.all():
|
2024-12-29 21:41:16 +01:00
|
|
|
if group.load_group_contribute_id:
|
|
|
|
contribute_to.add(group.load_group_contribute_id)
|
2024-12-29 20:01:53 +01:00
|
|
|
for beacon in beacons_by_space.get(space.pk, {}).values():
|
|
|
|
for load_group_id in contribute_to:
|
|
|
|
max_values[load_group_id] += beacon.max_observed_num_clients
|
|
|
|
current_values[load_group_id] += beacon.num_clients
|
|
|
|
|
2024-12-29 20:11:34 +01:00
|
|
|
result = {
|
2024-12-29 23:55:58 +01:00
|
|
|
pk: math.sin(3.14159 / 2 * (max(current_values[pk] / max_value - 0.2, 0) / 0.65)) if max_value else 0
|
2024-12-29 20:01:53 +01:00
|
|
|
for pk, max_value in max_values.items()
|
|
|
|
}
|
2024-12-29 20:11:34 +01:00
|
|
|
cache.set('mapdata:get_load', result, 300)
|
|
|
|
return result
|
2024-12-29 19:44:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
class ApLoadSchema(BaseSchema):
|
|
|
|
aps: dict[str, int]
|
|
|
|
|
2024-12-29 20:01:53 +01:00
|
|
|
|
2024-12-29 20:11:34 +01:00
|
|
|
@map_api_router.post('/load/', summary="update current load data",
|
|
|
|
response={204: None, **auth_permission_responses})
|
2024-12-29 19:44:36 +01:00
|
|
|
def post_load(request, parameters: ApLoadSchema):
|
2024-12-29 20:11:34 +01:00
|
|
|
if not request.user_permissions.can_write_load_data:
|
|
|
|
raise APIPermissionDenied()
|
2024-12-29 19:44:36 +01:00
|
|
|
|
|
|
|
names = parameters.aps.keys()
|
|
|
|
|
2024-12-29 20:11:34 +01:00
|
|
|
with transaction.atomic():
|
|
|
|
for beacon in RangingBeacon.objects.filter(ap_name__in=names):
|
|
|
|
beacon.num_clients = parameters.aps[beacon.ap_name]
|
|
|
|
if beacon.num_clients > beacon.max_observed_num_clients:
|
|
|
|
beacon.max_observed_num_clients = beacon.num_clients
|
|
|
|
beacon.save()
|
|
|
|
|
|
|
|
cache.delete('mapdata:get_load')
|
2024-12-29 22:17:49 +01:00
|
|
|
cache.set('mapdata:load_is_recent', True, 300)
|
2024-12-29 19:44:36 +01:00
|
|
|
|
|
|
|
return 204, None
|