From aa49840806eea637b4516f3ab4f33e1ac92720c4 Mon Sep 17 00:00:00 2001 From: Jenny Danzmayr Date: Tue, 13 Aug 2024 21:17:36 +0200 Subject: [PATCH] start of proj4 support (part of GPS support) --- src/c3nav/mapdata/api/map.py | 23 ++++++++++- src/c3nav/mapdata/schemas/models.py | 43 +++++++++++++++++++ src/c3nav/settings.py | 64 +++++++++++++++++++++++++++++ src/requirements/production.txt | 1 + 4 files changed, 129 insertions(+), 2 deletions(-) diff --git a/src/c3nav/mapdata/api/map.py b/src/c3nav/mapdata/api/map.py index a477ec18..c7683b83 100644 --- a/src/c3nav/mapdata/api/map.py +++ b/src/c3nav/mapdata/api/map.py @@ -9,6 +9,7 @@ from ninja import Router as APIRouter from pydantic import Field as APIField from pydantic import PositiveInt +from c3nav import settings from c3nav.api.auth import auth_permission_responses, auth_responses, validate_responses from c3nav.api.exceptions import API404, APIPermissionDenied, APIRequestValidationFailed from c3nav.api.schema import BaseSchema @@ -19,8 +20,9 @@ from c3nav.mapdata.models.locations import DynamicLocation, LocationRedirect, Po from c3nav.mapdata.schemas.filters import BySearchableFilter, RemoveGeometryFilter from c3nav.mapdata.schemas.model_base import AnyLocationID, AnyPositionID, CustomLocationID from c3nav.mapdata.schemas.models import (AnyPositionStatusSchema, FullListableLocationSchema, FullLocationSchema, - LocationDisplay, SlimListableLocationSchema, SlimLocationSchema, - all_location_definitions, listable_location_definitions) + LocationDisplay, ProjectionPipelineSchema, ProjectionSchema, + SlimListableLocationSchema, SlimLocationSchema, all_location_definitions, + listable_location_definitions) from c3nav.mapdata.schemas.responses import LocationGeometry, WithBoundsSchema 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) @@ -310,3 +312,20 @@ def set_position(request, position_id: AnyPositionID, update: UpdatePositionSche location.save() return location.serialize_position() + + +@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 diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index 92f1e7d0..957dc4c5 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -793,3 +793,46 @@ AnyPositionStatusSchema = Annotated[ ], Discriminator("available"), ] + + +class ProjectionPipelineSchema(BaseSchema): + pipeline: Union[ + Annotated[NonEmptyStr, APIField(title='proj4 string')], + Annotated[None, APIField(title='null', description='projection not available')] + ] = APIField( + title='proj4 string', + description='proj4 string for converting WGS84 coordinates to c3nav coordinates if available', + example='+proj=utm +zone=33 +ellps=GRS80 +units=m +no_defs' + ) + +class ProjectionSchema(ProjectionPipelineSchema): + proj4: NonEmptyStr = APIField( + title='proj4 string', + description='proj4 string for converting WGS84 coordinates to c3nav coordinates without offset and rotation', + example='+proj=utm +zone=33 +ellps=GRS80 +units=m +no_defs' + ) + zero_point: tuple[float, float] = APIField( + title='zero point', + description='coordinates of the zero point of the c3nav coordinate system', + example=(0.0, 0.0), + ) + rotation: float = APIField( + title='rotation', + description='rotational offset of the c3nav coordinate system', + example=0.0, + ) + rotation_matrix: Optional[tuple[ + float, float, float, float, + float, float, float, float, + float, float, float, float, + float, float, float, float, + ]] = APIField( + title='rotation matrix', + description='rotation matrix for rotational offset of the c3nav coordinate system', + example=[ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ] + ) diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index 33249264..d341cb9b 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -1,4 +1,5 @@ # c3nav settings, mostly taken from the pretix project +import math import os import re import string @@ -12,6 +13,7 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.crypto import get_random_string from django.utils.dateparse import parse_duration from django.utils.translation import gettext_lazy as _ +from pyproj import Proj, Transformer from c3nav import __version__ as c3nav_version from c3nav.utils.config import C3navConfigParser @@ -606,6 +608,68 @@ BASE_THEME = { WIFI_SSIDS = [n for n in config.get('c3nav', 'wifi_ssids', fallback='').split(',') if n] + +# Projection +PROJECTION_PROJ4 = config.get('projection', 'proj4', fallback=None) +PROJECTION_ZERO_POINT = config.get('projection', 'zero_point', fallback=None) +PROJECTION_ZERO_POINT_IS_WGS84 = '°' in PROJECTION_ZERO_POINT if PROJECTION_ZERO_POINT else False +PROJECTION_ROTATION = config.getfloat('projection', 'rotation', fallback=0.0) +PROJECTION_ROTATION_MATRIX = config.get('projection', 'rotation_matrix', fallback=None) +PROJECTION_TRANSFORMER: Optional[Transformer] = None +PROJECTION_TRANSFORMER_STRING: Optional[str] = None + +if PROJECTION_PROJ4: + if '+units=m' not in PROJECTION_PROJ4: + PROJECTION_PROJ4 += ' +units=m' + PROJECTION_TRANSFORMER_STRING = re.sub(r'\s?\+no_defs', '', PROJECTION_PROJ4) + + if (PROJECTION_ZERO_POINT or PROJECTION_ROTATION) and 'pipeline' not in PROJECTION_TRANSFORMER_STRING: + PROJECTION_TRANSFORMER_STRING = f'+proj=pipeline +step {PROJECTION_TRANSFORMER_STRING}' + + if PROJECTION_ZERO_POINT: + PROJECTION_ZERO_POINT = tuple((float(i) for i in PROJECTION_ZERO_POINT.split(','))) + if len(PROJECTION_ZERO_POINT) != 2: + raise ImproperlyConfigured(f'invalid projection zero point "{PROJECTION_ZERO_POINT!r}"') + if PROJECTION_ZERO_POINT_IS_WGS84: + PROJECTION_ZERO_POINT = Proj.from_pipeline(PROJECTION_PROJ4).transform(PROJECTION_ZERO_POINT[0], + PROJECTION_ZERO_POINT[1]) + PROJECTION_TRANSFORMER_STRING += (f' +step +proj=affine +xoff=-{PROJECTION_ZERO_POINT[0]} ' + f'+yoff=-{PROJECTION_ZERO_POINT[1]}') + + if PROJECTION_ROTATION != 0: + PROJECTION_ROTATION_MATRIX = ( + math.cos(math.radians(PROJECTION_ROTATION)), math.sin(math.radians(PROJECTION_ROTATION)), 0, 0, + -math.sin(math.radians(PROJECTION_ROTATION)), math.cos(math.radians(PROJECTION_ROTATION)), 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ) + elif PROJECTION_ROTATION_MATRIX: + PROJECTION_ROTATION_MATRIX = tuple((float(i) for i in PROJECTION_ROTATION_MATRIX.split(','))) + if len(PROJECTION_ROTATION_MATRIX) != 16: + raise ImproperlyConfigured(f'invalid rotation matrix "{PROJECTION_ROTATION_MATRIX!r}"') + + if PROJECTION_ROTATION_MATRIX: + PROJECTION_TRANSFORMER_STRING += ( + f' +step +proj=affine ' + f'+s11={PROJECTION_ROTATION_MATRIX[0]} +s12={PROJECTION_ROTATION_MATRIX[1]}' + ) + if PROJECTION_ROTATION_MATRIX[2] != 0: + PROJECTION_TRANSFORMER_STRING += f' +s13={PROJECTION_ROTATION_MATRIX[2]}' + PROJECTION_TRANSFORMER_STRING += f' +s21={PROJECTION_ROTATION_MATRIX[4]} +s22={PROJECTION_ROTATION_MATRIX[5]}' + if PROJECTION_ROTATION_MATRIX[6] != 0: + PROJECTION_TRANSFORMER_STRING += ' +s23={PROJECTION_ROTATION_MATRIX[6]}' + if PROJECTION_ROTATION_MATRIX[8] != 0: + PROJECTION_TRANSFORMER_STRING += f' +s31={PROJECTION_ROTATION_MATRIX[8]}' + if PROJECTION_ROTATION_MATRIX[9] != 0: + PROJECTION_TRANSFORMER_STRING += f' +s32={PROJECTION_ROTATION_MATRIX[9]}' + if PROJECTION_ROTATION_MATRIX[10] != 1: + PROJECTION_TRANSFORMER_STRING += f' +s33={PROJECTION_ROTATION_MATRIX[10]}' + if PROJECTION_ROTATION_MATRIX[15] != 1: + PROJECTION_TRANSFORMER_STRING += f' +tscale={PROJECTION_ROTATION_MATRIX[15]}' + + PROJECTION_TRANSFORMER_STRING += ' +no_defs' + PROJECTION_TRANSFORMER = Proj.from_pipeline(PROJECTION_TRANSFORMER_STRING) + USER_REGISTRATION = config.getboolean('c3nav', 'user_registration', fallback=True) INTERNAL_IPS = ('127.0.0.1', '::1') diff --git a/src/requirements/production.txt b/src/requirements/production.txt index 7addbc1e..d72b757c 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -20,3 +20,4 @@ django_libsass==0.9 channels==4.0.0 daphne==4.1.0 pyzstd==0.15.9 +pyproj==3.6.1