team-3/src/c3nav/mapdata/schemas/model_base.py
2024-12-03 16:56:55 +01:00

348 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import math
import re
from typing import Annotated, Optional, Union
from pydantic import Field as APIField
from pydantic import PositiveInt
from c3nav.api.schema import BaseSchema, LineStringSchema, PointSchema, PolygonSchema
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.models.geometry.base import GeometryMixin
from c3nav.mapdata.utils.geometry import smart_mapping
from c3nav.mapdata.utils.json import format_geojson
def schema_description(schema):
return schema.__doc__.replace("\n ", "\n").strip()
def schema_definition(schema):
return ("- **"+re.sub(r"([a-z])([A-Z])", r"\1 \2", schema.__name__.removesuffix("Schema")) + "**: " +
schema_description(schema).split("\n")[0].strip())
def schema_definitions(schemas):
return "\n".join(schema_definition(schema) for schema in schemas)
BoundsSchema = tuple[
Annotated[tuple[
Annotated[float, APIField(title="left", description="lowest X coordindate")],
Annotated[float, APIField(title="bottom", description="lowest Y coordindate")]
], APIField(title="(left, bottom)", description="lowest coordinates", example=(-10, -20))],
Annotated[tuple[
Annotated[float, APIField(title="right", description="highest X coordindate")],
Annotated[float, APIField(title="top", description="highest Y coordindate")]
], APIField(title="(right, top)", description="highest coordinates", example=(20, 30))]
]
class DjangoModelSchema(BaseSchema):
id: PositiveInt = APIField(
title="ID",
example=1,
)
class LocationSlugSchema(BaseSchema):
slug: Optional[NonEmptyStr] = APIField(
title="location slug",
description="a slug is a unique way to refer to a location. while locations have a shared ID space, slugs"
"are meants to be human-readable and easy to remember.",
example="entrance",
)
effective_slug: NonEmptyStr = APIField(
title="effective location slug",
description="if a location doesn't have a slug defined, this field holds a slug generated from the "
"location type and ID, which will work even if a human-readable slug is defined later.\n\n"
"even dynamic locations like coordinates have an (auto-generated) slug.",
example="entrance",
)
class WithAccessRestrictionSchema(BaseSchema):
access_restriction: Union[
Annotated[PositiveInt, APIField(title="access restriction ID")],
Annotated[None, APIField(title="null", description="no access restriction")],
] = APIField(
default=None,
title="access restriction ID",
description="access restriction that this object belongs to",
)
class TitledSchema(BaseSchema):
titles: dict[NonEmptyStr, NonEmptyStr] = APIField(
title="title (all languages)",
description="title in all available languages. property names are the ISO-language code. "
"languages may be missing.",
example={
"en": "Entrance",
"de": "Eingang",
}
)
title: NonEmptyStr = APIField(
title="title (preferred language)",
description="title in the preferred language based on the Accept-Language header.",
example="Entrance",
)
class LocationSchema(WithAccessRestrictionSchema, TitledSchema, LocationSlugSchema):
subtitle: NonEmptyStr = APIField(
title="subtitle (preferred language)",
description="an automatically generated short description for this location in the "
"preferred language based on the Accept-Language header.",
example="near Area 51",
)
icon: Optional[NonEmptyStr] = APIField( # todo: not optional?
title="icon name",
description="any material design icon name",
example="pin_drop",
)
can_search: bool = APIField(
title="can be searched",
description="if `true`, this object can show up in search results",
)
can_describe: bool = APIField(
title="can describe locations",
description="if `true`, this object can be used to describe other locations (e.g. in their subtitle)",
)
add_search: Union[
Annotated[str, APIField(title="search terms", description="set when looking for searchable locations")],
Annotated[None, APIField(title="null", description="when not looking for searchable locations")],
] = APIField(
None,
title="additional search terms",
description="more data for the search index separated by spaces, "
"only set when looking for searchable locations",
example="more search terms",
)
class LabelSettingsSchema(DjangoModelSchema): # todo: add titles back in here
"""
Settings preset for how and when to display a label. Reusable between objects.
The title describes the title of this preset, not the displayed label.
"""
min_zoom: float = APIField(
-10,
title="min zoom",
description="minimum zoom to display the label at",
)
max_zoom: float = APIField(
10,
title="max zoom",
description="maximum, zoom to display the label at",
)
font_size: PositiveInt = APIField(
title="font size",
description="font size of the label",
example=12,
)
class SpecificLocationSchema(LocationSchema):
grid_square: Union[
Annotated[NonEmptyStr, APIField(title="grid square", description="grid square(s) that this location is in")],
Annotated[None, APIField(title="null", description="no grid defined or outside of grid")],
] = APIField(
default=None,
title="grid square",
description="grid cell(s) that this location is in, if a grid is defined and the location is within it",
example="C3",
)
groups: list[PositiveInt] = APIField(
title="location groups",
description="location group(s) that this specific location belongs to.",
example=[5, 1, 3, 7],
)
groups_by_category: dict[
Annotated[NonEmptyStr, APIField(title="location group category name")],
Union[
Annotated[list[PositiveInt], APIField(
title="array of location IDs",
description="for categories that have `single` set to `false`. can be an empty array.",
example=[1, 4, 5],
)],
Annotated[PositiveInt, APIField(
title="one location ID",
description="for categories that have `single` set to `true`.",
example=1,
)],
Annotated[None, APIField(
title="null",
description="for categories that have `single` set to `true`."
)],
]
] = APIField(
title="location groups by category",
description="location group(s) that this specific location belongs to, grouped by categories.\n\n"
"keys are location group category names. see location group category endpoint for details.\n\n"
"categories may be missing if no groups apply.",
example={
"category_with_single_true": 5,
"other_category_with_single_true": None,
"category_with_single_false": [1, 3, 7],
}
)
label_settings: Optional[PositiveInt] = APIField(
default=None,
title="label settings",
description=(
schema_description(LabelSettingsSchema) +
"\n\nif not set, label settings of location groups might be used"
)
)
effective_label_settings: Union[
Annotated[LabelSettingsSchema, APIField(
title="label settings",
description="label settings to use",
)],
Annotated[None, APIField(
title="null",
description="label settings from location group will be used, if available"
)],
] = APIField(
default=None,
title="label settings",
description=(
schema_description(LabelSettingsSchema) +
"\n\nif not set, label settings of location groups might be used"
)
)
label_override: Union[
Annotated[NonEmptyStr, APIField(title="label override", description="text to use for label")],
Annotated[None, APIField(title="null", description="title will be used")],
] = APIField(
default=None,
title="label override (preferred language)",
description="text to use for the label. by default (null), the title would be used."
)
class WithGeometrySchema(BaseSchema):
@classmethod
def get_overrides(cls, value) -> dict:
value: GeometryMixin
if "geometry" in value.get_deferred_fields():
return {
**super().get_overrides(value),
"geometry": None,
"point": None,
"bounds": None,
}
minx, miny, maxx, maxy = value.geometry.bounds
return {
**super().get_overrides(value),
"geometry": format_geojson(smart_mapping(value.geometry), rounded=False),
"point": (value.level_id,) + tuple(round(i, 2) for i in value.point.coords[0]),
"bounds": ((int(math.floor(minx)), int(math.floor(miny))),
(int(math.ceil(maxx)), int(math.ceil(maxy))))
}
class WithPolygonGeometrySchema(WithGeometrySchema):
geometry: Union[
PolygonSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
] = APIField(
None,
title="geometry",
description="can be null if not available or excluded from endpoint",
)
class WithLineStringGeometrySchema(WithGeometrySchema):
geometry: Union[
LineStringSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
] = APIField(
None,
title="geometry",
description="can be null if not available or excluded from endpoint",
)
class WithPointGeometrySchema(WithGeometrySchema):
geometry: Union[
PointSchema,
Annotated[None, APIField(title="null", description="geometry not available of excluded from endpoint")]
] = APIField(
None,
title="geometry",
description="can be null if not available or excluded from endpoint",
)
class WithLevelSchema(BaseSchema):
level: PositiveInt = APIField(
title="level",
description="level id this object belongs to.",
example=1,
)
class WithSpaceSchema(BaseSchema):
space: PositiveInt = APIField(
title="space",
description="space id this object belongs to.",
example=1,
)
class SimpleGeometryPointSchema(BaseSchema):
point: tuple[
Annotated[PositiveInt, APIField(title="level ID")],
Annotated[float, APIField(title="x coordinate")],
Annotated[float, APIField(title="y coordinate")]
] = APIField(
title="point representation",
description="representative point for the location",
example=(1, 4.2, 13.37)
)
class SimpleGeometryPointAndBoundsSchema(SimpleGeometryPointSchema):
bounds: BoundsSchema = APIField(
description="location bounding box",
example=((-10, -20), (20, 30)),
)
class SimpleGeometryLocationsSchema(BaseSchema):
locations: list[PositiveInt] = APIField( # todo: this should be a set… but json serialization?
description="IDs of all locations that belong to this grouo",
example=(1, 2, 3),
)
CustomLocationID = Annotated[NonEmptyStr, APIField(
title="custom location ID",
pattern=r"c:[a-z0-9-_]+:(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)$",
example="c:0:-7.23:12.34",
description="level short_name and x/y coordinates form the ID of a custom location"
)]
PositionID = Annotated[NonEmptyStr, APIField(
title="position ID",
pattern=r"p:[A-Za-z0-9]+$",
description="the ID of a user-defined tracked position is made up of its secret"
)]
Coordinates3D = tuple[float, float, float]
AnyLocationID = Union[
Annotated[PositiveInt, APIField(
title="location ID",
description="numeric ID of any lcation all locations have a shared ID space"
)],
CustomLocationID,
PositionID,
]
AnyPositionID = Union[
Annotated[PositiveInt, APIField(
title="dynamic location ID",
description="numeric ID of any dynamic lcation"
)],
PositionID,
]