diff --git a/src/c3nav/mapdata/migrations/0107_altitudearea_multiple_points.py b/src/c3nav/mapdata/migrations/0107_altitudearea_multiple_points.py new file mode 100644 index 00000000..55018ba1 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0107_altitudearea_multiple_points.py @@ -0,0 +1,67 @@ +# Generated by Django 5.0.8 on 2024-08-17 17:50 + +import django.core.serializers.json +import django_pydantic_field.compat.django +import django_pydantic_field.fields +import types +import typing +from django.db import migrations, models +from shapely.geometry import Point + +from c3nav.mapdata.models.geometry.level import AltitudeAreaPoint + + +def forwards_func(apps, schema_editor): + AltitudeArea = apps.get_model('mapdata', 'AltitudeArea') + for area in AltitudeArea.objects.all(): + if area.point1 is not None: + area.points = [ + AltitudeAreaPoint(coordinates=[area.point1.x, area.point1.y], altitude=float(area.altitude1)), + AltitudeAreaPoint(coordinates=[area.point2.x, area.point2.y], altitude=float(area.altitude)) + ] + area.altitude = None + area.save() + + +def backwards_func(apps, schema_editor): + AltitudeArea = apps.get_model('mapdata', 'AltitudeArea') + for area in AltitudeArea.objects.all(): + if area.points is not None: + area.point1 = Point(*area.points[0].coordinates) + area.point2 = Point(*area.points[-1].coordinates) + area.altitude = area.points[0].altitude + area.altitude2 = area.points[-1].altitude + area.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0106_rename_wifi_to_beaconmeasurement'), + ] + + operations = [ + migrations.AddField( + model_name='altitudearea', + name='points', + field=django_pydantic_field.fields.PydanticSchemaField(config=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True, schema=django_pydantic_field.compat.django.GenericContainer(typing.Union, (django_pydantic_field.compat.django.GenericContainer(list, (AltitudeAreaPoint,)), types.NoneType))), + ), + migrations.AlterField( + model_name='altitudearea', + name='altitude', + field=models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='altitude'), + ), + migrations.RunPython(forwards_func, backwards_func), + migrations.RemoveField( + model_name='altitudearea', + name='altitude2', + ), + migrations.RemoveField( + model_name='altitudearea', + name='point1', + ), + migrations.RemoveField( + model_name='altitudearea', + name='point2', + ), + ] diff --git a/src/c3nav/mapdata/models/geometry/level.py b/src/c3nav/mapdata/models/geometry/level.py index f7920a74..b40f35ca 100644 --- a/src/c3nav/mapdata/models/geometry/level.py +++ b/src/c3nav/mapdata/models/geometry/level.py @@ -3,20 +3,26 @@ from collections import deque from decimal import Decimal from itertools import chain, combinations from operator import attrgetter, itemgetter +from typing import Sequence, Optional import numpy as np from django.core.validators import MinValueValidator from django.db import models +from django.db.models import CheckConstraint, Q from django.urls import reverse from django.utils.functional import cached_property from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ +from pydantic import Field as APIField +from scipy.interpolate._rbfinterp import RBFInterpolator from shapely import prepared from shapely.affinity import scale -from shapely.geometry import JOIN_STYLE, LineString, MultiPolygon +from shapely.geometry import JOIN_STYLE, LineString, MultiPolygon, Point from shapely.geometry.polygon import orient +from django_pydantic_field import SchemaField from shapely.ops import unary_union +from c3nav.api.schema import BaseSchema from c3nav.mapdata.fields import GeometryField, I18nField from c3nav.mapdata.grid import grid from c3nav.mapdata.models import Level @@ -170,15 +176,26 @@ class ItemWithValue: return self._func() +class AltitudeAreaPoint(BaseSchema): + coordinates: tuple[float, float] = APIField( + example=[1, 2.5] + ) + altitude: float + + class AltitudeArea(LevelGeometryMixin, models.Model): """ An altitude area """ - geometry = GeometryField('multipolygon') - altitude = models.DecimalField(_('altitude'), null=False, max_digits=6, decimal_places=2) - altitude2 = models.DecimalField(_('second altitude'), null=True, max_digits=6, decimal_places=2) - point1 = GeometryField('point', null=True) - point2 = GeometryField('point', null=True) + geometry: MultiPolygon = GeometryField('multipolygon') + altitude = models.DecimalField(_('altitude'), null=True, max_digits=6, decimal_places=2) + points: Sequence[AltitudeAreaPoint] = SchemaField(schema=list[AltitudeAreaPoint], null=True) + + constraints = ( + CheckConstraint(check=(Q(points__isnull=True, altitude__isnull=False) | + Q(points__isnull=False, altitude__isnull=True)), + name="altitudearea_needs_precisely_one_of_altitude_or_points"), + ) class Meta: verbose_name = _('Altitude Area')