diff options
| author | Étienne Loks <etienne.loks@iggdrasil.net> | 2022-02-14 19:42:49 +0100 | 
|---|---|---|
| committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2022-12-12 12:21:00 +0100 | 
| commit | b93a677cc30d92a0efa0ca8be52614ed1eb67329 (patch) | |
| tree | e896479281059a23d5ceaa109ea8eba2e78ae2f0 | |
| parent | 5ecc278a4beded9d64b02744c8b80f39c8c04e19 (diff) | |
| download | Ishtar-b93a677cc30d92a0efa0ca8be52614ed1eb67329.tar.bz2 Ishtar-b93a677cc30d92a0efa0ca8be52614ed1eb67329.zip | |
Geodata redesign:  town migrations
| -rw-r--r-- | archaeological_context_records/migrations/0107_auto_20220214_1920.py (renamed from archaeological_context_records/migrations/0107_auto_20220211_1630.py) | 4 | ||||
| -rw-r--r-- | archaeological_files/migrations/0107_auto_20220214_1920.py (renamed from archaeological_files/migrations/0107_auto_20220211_1630.py) | 2 | ||||
| -rw-r--r-- | archaeological_finds/migrations/0109_auto_20220214_1920.py (renamed from archaeological_finds/migrations/0109_auto_20220211_1630.py) | 4 | ||||
| -rw-r--r-- | archaeological_operations/migrations/0108_auto_20220214_1920.py (renamed from archaeological_operations/migrations/0108_auto_20220211_1630.py) | 2 | ||||
| -rw-r--r-- | archaeological_operations/migrations/0109_auto_20220214_1920.py (renamed from archaeological_operations/migrations/0109_auto_20220211_1630.py) | 6 | ||||
| -rw-r--r-- | archaeological_warehouse/migrations/0113_auto_20220214_1920.py (renamed from archaeological_warehouse/migrations/0113_auto_20220211_1630.py) | 4 | ||||
| -rw-r--r-- | ishtar_common/admin.py | 5 | ||||
| -rw-r--r-- | ishtar_common/management/commands/migrate_to_geo_v4.py | 98 | ||||
| -rw-r--r-- | ishtar_common/migrations/0220_auto_20220214_1920.py (renamed from ishtar_common/migrations/0220_auto_20220211_1630.py) | 12 | ||||
| -rw-r--r-- | ishtar_common/models_common.py | 671 | 
10 files changed, 470 insertions, 338 deletions
| diff --git a/archaeological_context_records/migrations/0107_auto_20220211_1630.py b/archaeological_context_records/migrations/0107_auto_20220214_1920.py index 725cdbf7e..1e507d219 100644 --- a/archaeological_context_records/migrations/0107_auto_20220211_1630.py +++ b/archaeological_context_records/migrations/0107_auto_20220214_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 +# Generated by Django 2.2.24 on 2022-02-14 19:20  import django.contrib.postgres.fields.jsonb  from django.db import migrations, models @@ -8,7 +8,7 @@ import django.db.models.deletion  class Migration(migrations.Migration):      dependencies = [ -        ('ishtar_common', '0220_auto_20220211_1630'), +        ('ishtar_common', '0220_auto_20220214_1920'),          ('archaeological_context_records', '0106_auto_20210326_1434'),      ] diff --git a/archaeological_files/migrations/0107_auto_20220211_1630.py b/archaeological_files/migrations/0107_auto_20220214_1920.py index b5bcaebf1..ba14f1c46 100644 --- a/archaeological_files/migrations/0107_auto_20220211_1630.py +++ b/archaeological_files/migrations/0107_auto_20220214_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 +# Generated by Django 2.2.24 on 2022-02-14 19:20  import django.contrib.postgres.fields.jsonb  from django.db import migrations, models diff --git a/archaeological_finds/migrations/0109_auto_20220211_1630.py b/archaeological_finds/migrations/0109_auto_20220214_1920.py index 42421f3ce..90df20f3e 100644 --- a/archaeological_finds/migrations/0109_auto_20220211_1630.py +++ b/archaeological_finds/migrations/0109_auto_20220214_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 +# Generated by Django 2.2.24 on 2022-02-14 19:20  import archaeological_finds.models_treatments  import django.contrib.postgres.fields.jsonb @@ -9,7 +9,7 @@ import django.db.models.deletion  class Migration(migrations.Migration):      dependencies = [ -        ('ishtar_common', '0220_auto_20220211_1630'), +        ('ishtar_common', '0220_auto_20220214_1920'),          ('archaeological_finds', '0108_auto_20210602_2234'),      ] diff --git a/archaeological_operations/migrations/0108_auto_20220211_1630.py b/archaeological_operations/migrations/0108_auto_20220214_1920.py index 2a6f2d313..57f73219c 100644 --- a/archaeological_operations/migrations/0108_auto_20220211_1630.py +++ b/archaeological_operations/migrations/0108_auto_20220214_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 +# Generated by Django 2.2.24 on 2022-02-14 19:20  from django.db import migrations diff --git a/archaeological_operations/migrations/0109_auto_20220211_1630.py b/archaeological_operations/migrations/0109_auto_20220214_1920.py index 12f509af1..e5ef7152e 100644 --- a/archaeological_operations/migrations/0109_auto_20220211_1630.py +++ b/archaeological_operations/migrations/0109_auto_20220214_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 +# Generated by Django 2.2.24 on 2022-02-14 19:20  import django.contrib.postgres.fields.jsonb  from django.db import migrations, models @@ -8,8 +8,8 @@ import django.db.models.deletion  class Migration(migrations.Migration):      dependencies = [ -        ('archaeological_operations', '0108_auto_20220211_1630'), -        ('ishtar_common', '0220_auto_20220211_1630'), +        ('archaeological_operations', '0108_auto_20220214_1920'), +        ('ishtar_common', '0220_auto_20220214_1920'),      ]      operations = [ diff --git a/archaeological_warehouse/migrations/0113_auto_20220211_1630.py b/archaeological_warehouse/migrations/0113_auto_20220214_1920.py index 4b758ac05..1892b8adf 100644 --- a/archaeological_warehouse/migrations/0113_auto_20220211_1630.py +++ b/archaeological_warehouse/migrations/0113_auto_20220214_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 +# Generated by Django 2.2.24 on 2022-02-14 19:20  import django.contrib.postgres.fields.jsonb  from django.db import migrations, models @@ -8,7 +8,7 @@ import django.db.models.deletion  class Migration(migrations.Migration):      dependencies = [ -        ('ishtar_common', '0220_auto_20220211_1630'), +        ('ishtar_common', '0220_auto_20220214_1920'),          ('archaeological_warehouse', '0112_auto_20210308_1628'),      ] diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index 0ef72991f..b7cc5adf6 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -1022,7 +1022,7 @@ class TownAdmin(ImportGEOJSONActionAdmin, ImportActionAdmin):          search_fields += ["numero_insee"]          list_filter = ("areas",)      form = AdminTownForm -    autocomplete_fields = ["children"] +    autocomplete_fields = ["children", "main_geodata", "geodata"]      inlines = [TownParentInline]      actions = [          export_as_csv_action(exclude=["center", "limit"]), @@ -2445,6 +2445,9 @@ class GeoVectorDataForm(forms.ModelForm):  class GeoVectorDataAdmin(admin.ModelAdmin):      model = models_common.GeoVectorData +    search_fields = ["name"] +    list_display = ["name", "origin", "data_type", "provider", "source_content_type"] +    list_filter = ["origin", "data_type", "provider"]  admin_site.register(models_common.GeoVectorData, GeoVectorDataAdmin)
\ No newline at end of file diff --git a/ishtar_common/management/commands/migrate_to_geo_v4.py b/ishtar_common/management/commands/migrate_to_geo_v4.py new file mode 100644 index 000000000..f747cb72d --- /dev/null +++ b/ishtar_common/management/commands/migrate_to_geo_v4.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from django.contrib.contenttypes.models import ContentType +import csv +import datetime +import os +import sys + +from django.conf import settings +from django.core.management.base import BaseCommand + +from ishtar_common import models_common + + +log_path = os.sep.join([settings.ROOT_PATH, "logs"]) +if not os.path.exists(log_path): +    os.mkdir(log_path, mode=0o770) + + +def migrate(quiet=False, log=True): +    changed = [] +    # create towns +    q = models_common.Town.objects.exclude( +        center__isnull=True, limit__isnull=True).exclude(main_geodata__isnull=False) +    nb = q.count() +    town_content_type = ContentType.objects.get(app_label='ishtar_common', model='town') +    data_type, __ = models_common.GeoDataType.objects.get_or_create( +        txt_idx="town-limit", defaults={"label": "Limites commune"}) +    provider, __ = models_common.GeoProviderType.objects.get_or_create( +        txt_idx="france-ign", defaults={"label": "IGN"}) +    for idx, town in enumerate(q.all()): +        if not quiet: +            sys.stdout.write(f"\r[{percent(idx, nb)}] Migrate towns {idx + 1}/{nb}") +            sys.stdout.flush() +        attrs = { +            "name": town._generate_cached_label(), +            "source_content_type": town_content_type, +            "source_id": town.pk, +            "data_type": data_type, +            "provider": provider, +        } +        if town.limit: +            attrs["multi_polygon"] = town.limit +        else: +            attrs["point_2d"] = town.center +        data, created = models_common.GeoVectorData.objects.get_or_create(**attrs) +        if created: +            changed.append(["geovectordata", data.name, data.pk]) +        town.main_geodata = data +        town.save() + +    if log and changed: +        filename = f"geo_migration-created-{get_time().replace(':', '')}.txt" +        path = os.sep.join([log_path, filename]) +        with open(path, 'w+') as fle: +            writer = csv.writer(fle) +            writer.writerow(["model", "name", "id"]) +            for change in changed: +                writer.writerow(change) +        if not quiet: +            sys.stdout.write(f"log: {path} written.") +    if not quiet: +        sys.stdout.write("\n") + + +def percent(current, total): +    return f"{(current + 1) / total * 100:.1f}".rjust(4, "0") + "%" + + +def get_time(): +    return datetime.datetime.now().isoformat().split(".")[0] + + +class Command(BaseCommand): +    help = "Migrate to new geo data management" + +    def add_arguments(self, parser): +        parser.add_argument( +            "--quiet", dest="quiet", action="store_true", help="Quiet output" +        ) +        parser.add_argument( +            "--log", dest="log", action="store_false", help="Log into a file" +        ) + +    def handle(self, *args, **options): +        log = options["log"] +        quiet = options["quiet"] +        if not quiet: +            sys.stdout.write(f"[{get_time()}] Processing migration\n") +        errors = migrate(quiet=quiet, log=log) +        if not errors: +            if not quiet: +                sys.stdout.write(f"[{get_time()}] Migration finished\n") +            sys.exit() +        if not quiet: +            sys.stdout.write("\n".join(errors)) +        sys.exit(1) diff --git a/ishtar_common/migrations/0220_auto_20220211_1630.py b/ishtar_common/migrations/0220_auto_20220214_1920.py index 159b65fcd..2f40eab00 100644 --- a/ishtar_common/migrations/0220_auto_20220211_1630.py +++ b/ishtar_common/migrations/0220_auto_20220214_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2022-02-11 16:30 +# Generated by Django 2.2.24 on 2022-02-14 19:20  import django.contrib.gis.db.models.fields  import django.core.validators @@ -111,4 +111,14 @@ class Migration(migrations.Migration):                  'verbose_name_plural': 'Geographic - Vector data',              },          ), +        migrations.AddField( +            model_name='town', +            name='geodata', +            field=models.ManyToManyField(blank=True, related_name='related_items_ishtar_common_town', to='ishtar_common.GeoVectorData'), +        ), +        migrations.AddField( +            model_name='town', +            name='main_geodata', +            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_related_items_ishtar_common_town', to='ishtar_common.GeoVectorData'), +        ),      ] diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py index ebf480ff0..cc3c61b05 100644 --- a/ishtar_common/models_common.py +++ b/ishtar_common/models_common.py @@ -1985,12 +1985,356 @@ class Canton(models.Model):          return settings.JOINT.join((self.name, str(self.arrondissement))) +class SpatialReferenceSystem(GeneralType): +    order = models.IntegerField(_("Order"), default=10) +    auth_name = models.CharField(_("Authority name"), default="EPSG", max_length=256) +    srid = models.IntegerField(_("Authority SRID")) + +    class Meta: +        verbose_name = _("Spatial reference system") +        verbose_name_plural = _("Spatial reference systems") +        ordering = ( +            "order", +            "label", +        ) + +    @classmethod +    def get_documentation_string(cls): +        """ +        Used for automatic documentation generation +        """ +        doc = super(SpatialReferenceSystem, cls).get_documentation_string() +        doc += ", **srid** {}, **auth_name** {}".format( +            _("Authority SRID"), _("Authority name") +        ) +        return doc + + +post_save.connect(post_save_cache, sender=SpatialReferenceSystem) +post_delete.connect(post_save_cache, sender=SpatialReferenceSystem) + + +class GeoOriginType(HierarchicalType): +    """ +    ex: topographical surveys, georeferencing, ... +    """ + +    order = models.IntegerField(_("Order"), default=10) + +    class Meta: +        verbose_name = _("Geographic - Origin type") +        verbose_name_plural = _("Geographic - Origin types") +        ordering = ( +            "order", +            "label", +        ) + + +class GeoDataType(HierarchicalType): +    """ +    ex: outline, z-sup, ... +    """ + +    order = models.IntegerField(_("Order"), default=10) + +    class Meta: +        verbose_name = _("Geographic - Data type") +        verbose_name_plural = _("Geographic - Data types") +        ordering = ( +            "order", +            "label", +        ) + + +class GeoProviderType(HierarchicalType): +    """ +    ex: GeoNames, IGN, ... +    """ + +    order = models.IntegerField(_("Order"), default=10) + +    class Meta: +        verbose_name = _("Geographic - Provider type") +        verbose_name_plural = _("Geographic - Provider types") +        ordering = ( +            "order", +            "label", +        ) + + +class GeoVectorData(models.Model): +    name = models.CharField(_("Name"), default=_("Default"), max_length=200) +    source_content_type = models.ForeignKey( +        ContentType, related_name="content_type_geovectordata", on_delete=models.CASCADE +    ) +    source_id = models.PositiveIntegerField() +    source = GenericForeignKey("source_content_type", "source_id") +    origin = models.ForeignKey( +        GeoOriginType, +        blank=True, +        null=True, +        on_delete=models.PROTECT, +        verbose_name=_("Origin"), +    ) +    data_type = models.ForeignKey( +        GeoDataType, +        blank=True, +        null=True, +        on_delete=models.PROTECT, +        verbose_name=_("Data type"), +    ) +    provider = models.ForeignKey( +        GeoProviderType, +        blank=True, +        null=True, +        on_delete=models.PROTECT, +        verbose_name=_("Provider"), +    ) +    comment = models.TextField(_("Comment"), default="", blank=True) +    x = models.FloatField(_("X"), blank=True, null=True, help_text=_("User input")) +    y = models.FloatField(_("Y"), blank=True, null=True, help_text=_("User input")) +    z = models.FloatField(_("Z"), blank=True, null=True, help_text=_("User input")) +    # x == cached_x if user input else get it from other sources +    # cached is converted to the display SRID +    cached_x = models.FloatField(_("X (cached)"), blank=True, null=True) +    cached_y = models.FloatField(_("Y (cached)"), blank=True, null=True) +    cached_z = models.FloatField(_("Z (cached)"), blank=True, null=True) +    estimated_error_x = models.FloatField( +        _("Estimated error for X"), blank=True, null=True +    ) +    estimated_error_y = models.FloatField( +        _("Estimated error for Y"), blank=True, null=True +    ) +    estimated_error_z = models.FloatField( +        _("Estimated error for Z"), blank=True, null=True +    ) +    spatial_reference_system = models.ForeignKey( +        SpatialReferenceSystem, +        verbose_name=_("Spatial Reference System"), +        blank=True, +        null=True, +        on_delete=models.PROTECT, +    ) +    point_2d = models.PointField(_("Point (2D)"), blank=True, null=True) +    point_3d = models.PointField(_("Point (3D)"), blank=True, null=True, dim=3) +    multi_points = models.MultiPointField(_("Multi points"), blank=True, null=True) +    multi_line = models.MultiLineStringField(_("Multi line"), blank=True, null=True) +    multi_polygon = models.MultiPolygonField(_("Multi polygon"), blank=True, null=True) +    need_update = models.BooleanField(_("Need update"), default=False) + +    class Meta: +        verbose_name = _("Geographic - Vector data") +        verbose_name_plural = _("Geographic - Vector data") + +    def __str__(self): +        name = self.name +        if self.data_type: +            name += f" ({str(self.data_type).lower()})" +        return name + +    def display_coordinates(self, rounded=5, dim=2, cache=True): +        srid = None +        profile = get_current_profile() +        if profile.display_srs and profile.display_srs.srid: +            srid = profile.display_srs.srid +        return self.get_coordinates(rounded=rounded, srid=srid, dim=dim, cache=cache) + +    def get_coordinates(self, rounded=5, srid: int = None, dim=2, cache=False): +        if dim not in (2, 3): +            raise ValueError(_("Only 2 or 3 dimension")) +        if cache: +            coordinates = [self.cached_x, self.cached_y] +            if dim == 3: +                coordinates.append(self.cached_z) +        else: +            if self.x and self.y:  # user input +                if not srid or srid == self.spatial_reference_system.srid: +                    coordinates = [self.x, self.y] +                    if dim == 3: +                        coordinates.append(self.z) +                else: +                    args = { +                        "x": self.x, +                        "y": self.y, +                        "srid": self.spatial_reference_system.srid, +                    } +                    if dim == 3: +                        args["z"] = self.z +                    point = Point(**args).transform(srid, clone=True) +                    coordinates = [point.x, point.y] +                    if dim == 3: +                        coordinates.append(point.z) +            elif self.point_2d and dim == 2: +                point = self.point_2d.transform(srid, clone=True) +                coordinates = [point.x, point.y] +            elif self.point_3d and dim == 3: +                point = self.point_3d.transform(srid, clone=True) +                coordinates = [point.x, point.y, point.z] +            else: +                return +        if not rounded: +            return coordinates +        return [round(coord, rounded) for coord in coordinates] + +    def get_coordinates_from_polygon(self, rounded=5, srid: int = None): +        if self.multi_polygon: +            return self.convert_coordinates( +                self.multi_polygon.centroid, rounded=rounded, srid=srid +            ) + +    def get_x(self, srid: int = None) -> float: +        coord = self.get_coordinates(srid) +        if coord: +            return coord[0] + +    def get_y(self, srid: int = None) -> float: +        coord = self.get_coordinates(srid) +        if coord: +            return coord[1] + +    def get_z(self, srid: int = None) -> float: +        coord = self.get_coordinates(srid, dim=3) +        if coord: +            return coord[2] + +    @property +    def display_spatial_reference_system(self): +        profile = get_current_profile() +        if not profile.display_srs or not profile.display_srs.srid: +            return self.spatial_reference_system +        return profile.display_srs + +    def get_geo_items(self, get_polygons, rounded=5): +        label = self.label if hasattr(self, "label") else self.short_label +        dct = {"type": "Feature", "geometry": {}, "properties": {"label": label}} +        if get_polygons: +            list_coords = [] +            if self.multi_polygon: +                for polygon in self.multi_polygon: +                    list_coords.append([]) +                    for linear_ring in range(len(polygon)): +                        list_coords[-1].append([]) +                        for coords in polygon[linear_ring].coords: +                            point_2d = Point( +                                coords[0], coords[1], srid=self.multi_polygon.srid +                            ) +                            list_coords[-1][linear_ring].append( +                                self.convert_coordinates(point_2d, rounded) +                            ) +            dct["geometry"]["type"] = "MultiPolygon" +            dct["geometry"]["coordinates"] = list_coords +        else: +            dct["geometry"]["type"] = "Point" +            coords = self.display_coordinates() +            if coords: +                dct["geometry"]["coordinates"] = coords +            elif self.multi_polygon: +                dct["geometry"]["coordinates"] = self.convert_coordinates( +                    self.multi_polygon.centroid, rounded +                ) +            else: +                return {} +        return dct + +    def convert_coordinates(self, point_2d, rounded=5, srid=None): +        if not srid: +            profile = get_current_profile() +            if profile.display_srs and profile.display_srs.srid: +                srid = profile.display_srs.srid +        if not srid: +            x, y = point_2d.x, point_2d.y +        else: +            point = point_2d.transform(srid, clone=True) +            x, y = point.x, point.y +        if rounded: +            return [round(x, rounded), round(y, rounded)] +        return [x, y] + +    def most_precise_geo(self): +        if self.multi_polygon: +            return "multi_polygon" +        if self.point_2d: +            return "point" + +    def _geojson_serialize(self, geom_attr): +        if not hasattr(self, geom_attr): +            return "" +        geojson = serialize( +            "geojson", +            self.__class__.objects.filter(pk=self.pk), +            geometry_field=geom_attr, +            fields=("name",), +        ) +        geojson_dct = json.loads(geojson) +        profile = get_current_profile() +        precision = profile.point_precision + +        features = geojson_dct.pop("features") +        for idx in range(len(features)): +            feature = features[idx] +            lbl = feature["properties"].pop("name") +            feature["properties"]["name"] = lbl +            feature["properties"]["id"] = self.pk +            if precision is not None: +                geom_type = feature["geometry"].get("type", None) +                if geom_type == "Point": +                    feature["geometry"]["coordinates"] = [ +                        round(coord, precision) +                        for coord in feature["geometry"]["coordinates"] +                    ] +        geojson_dct["features"] = features +        geojson_dct["link_template"] = simple_link_to_window(self).replace( +            "999999", "<pk>" +        ) +        geojson = json.dumps(geojson_dct) +        return geojson + +    @property +    def point_2d_geojson(self): +        return self._geojson_serialize("point_2d") + +    @property +    def multi_polygon_geojson(self): +        return self._geojson_serialize("multi_polygon") + + +post_save.connect(post_save_geodata, sender=GeoVectorData) + + +class GeographicItem(models.Model): +    main_geodata = models.ForeignKey( +        GeoVectorData, +        on_delete=models.SET_NULL, +        blank=True, +        null=True, +        related_name="main_related_items_%(app_label)s_%(class)s", +    ) +    geodata = models.ManyToManyField( +        GeoVectorData, blank=True, related_name="related_items_%(app_label)s_%(class)s" +    ) + +    class Meta: +        abstract = True + +    def save( +        self, force_insert=False, force_update=False, using=None, update_fields=None +    ): +        super(GeographicItem, self).save( +            force_insert=force_insert, +            force_update=force_update, +            using=using, +            update_fields=update_fields, +        ) +        if self.main_geodata and not self.geodata.filter(pk=self.main_geodata.pk): +            self.geodata.add(self.main_geodata) + +  class TownManager(models.Manager):      def get_by_natural_key(self, numero_insee, year):          return self.get(numero_insee=numero_insee, year=year) -class Town(Imported, models.Model): +class Town(GeographicItem, Imported, models.Model):      name = models.CharField(_("Name"), max_length=100)      surface = models.IntegerField(_("Surface (m2)"), blank=True, null=True)      center = models.PointField( @@ -2868,330 +3212,7 @@ class DynamicRequest:          return alt_names -class SpatialReferenceSystem(GeneralType): -    order = models.IntegerField(_("Order"), default=10) -    auth_name = models.CharField(_("Authority name"), default="EPSG", max_length=256) -    srid = models.IntegerField(_("Authority SRID")) - -    class Meta: -        verbose_name = _("Spatial reference system") -        verbose_name_plural = _("Spatial reference systems") -        ordering = ( -            "order", -            "label", -        ) - -    @classmethod -    def get_documentation_string(cls): -        """ -        Used for automatic documentation generation -        """ -        doc = super(SpatialReferenceSystem, cls).get_documentation_string() -        doc += ", **srid** {}, **auth_name** {}".format( -            _("Authority SRID"), _("Authority name") -        ) -        return doc - - -post_save.connect(post_save_cache, sender=SpatialReferenceSystem) -post_delete.connect(post_save_cache, sender=SpatialReferenceSystem) - - -class GeoOriginType(HierarchicalType): -    """ -    ex: topographical surveys, georeferencing, ... -    """ - -    order = models.IntegerField(_("Order"), default=10) - -    class Meta: -        verbose_name = _("Geographic - Origin type") -        verbose_name_plural = _("Geographic - Origin types") -        ordering = ( -            "order", -            "label", -        ) - - -class GeoDataType(HierarchicalType): -    """ -    ex: outline, z-sup, ... -    """ - -    order = models.IntegerField(_("Order"), default=10) - -    class Meta: -        verbose_name = _("Geographic - Data type") -        verbose_name_plural = _("Geographic - Data types") -        ordering = ( -            "order", -            "label", -        ) - - -class GeoProviderType(HierarchicalType): -    """ -    ex: GeoNames, IGN, ... -    """ - -    order = models.IntegerField(_("Order"), default=10) - -    class Meta: -        verbose_name = _("Geographic - Provider type") -        verbose_name_plural = _("Geographic - Provider types") -        ordering = ( -            "order", -            "label", -        ) - - -class GeoVectorData(models.Model): -    name = models.CharField(_("Name"), default=_("Default"), max_length=200) -    source_content_type = models.ForeignKey( -        ContentType, related_name="content_type_geovectordata", on_delete=models.CASCADE -    ) -    source_id = models.PositiveIntegerField() -    source = GenericForeignKey("source_content_type", "source_id") -    origin = models.ForeignKey( -        GeoOriginType, -        blank=True, -        null=True, -        on_delete=models.PROTECT, -        verbose_name=_("Origin"), -    ) -    data_type = models.ForeignKey( -        GeoDataType, -        blank=True, -        null=True, -        on_delete=models.PROTECT, -        verbose_name=_("Data type"), -    ) -    provider = models.ForeignKey( -        GeoProviderType, -        blank=True, -        null=True, -        on_delete=models.PROTECT, -        verbose_name=_("Provider"), -    ) -    comment = models.TextField(_("Comment"), default="", blank=True) -    x = models.FloatField(_("X"), blank=True, null=True, help_text=_("User input")) -    y = models.FloatField(_("Y"), blank=True, null=True, help_text=_("User input")) -    z = models.FloatField(_("Z"), blank=True, null=True, help_text=_("User input")) -    # x == cached_x if user input else get it from other sources -    # cached is converted to the display SRID -    cached_x = models.FloatField(_("X (cached)"), blank=True, null=True) -    cached_y = models.FloatField(_("Y (cached)"), blank=True, null=True) -    cached_z = models.FloatField(_("Z (cached)"), blank=True, null=True) -    estimated_error_x = models.FloatField( -        _("Estimated error for X"), blank=True, null=True -    ) -    estimated_error_y = models.FloatField( -        _("Estimated error for Y"), blank=True, null=True -    ) -    estimated_error_z = models.FloatField( -        _("Estimated error for Z"), blank=True, null=True -    ) -    spatial_reference_system = models.ForeignKey( -        SpatialReferenceSystem, -        verbose_name=_("Spatial Reference System"), -        blank=True, -        null=True, -        on_delete=models.PROTECT, -    ) -    point_2d = models.PointField(_("Point (2D)"), blank=True, null=True) -    point_3d = models.PointField(_("Point (3D)"), blank=True, null=True, dim=3) -    multi_points = models.MultiPointField(_("Multi points"), blank=True, null=True) -    multi_line = models.MultiLineStringField(_("Multi line"), blank=True, null=True) -    multi_polygon = models.MultiPolygonField(_("Multi polygon"), blank=True, null=True) -    need_update = models.BooleanField(_("Need update"), default=False) - -    class Meta: -        verbose_name = _("Geographic - Vector data") -        verbose_name_plural = _("Geographic - Vector data") - -    def display_coordinates(self, rounded=5, dim=2, cache=True): -        srid = None -        profile = get_current_profile() -        if profile.display_srs and profile.display_srs.srid: -            srid = profile.display_srs.srid -        return self.get_coordinates(rounded=rounded, srid=srid, dim=dim, cache=cache) - -    def get_coordinates(self, rounded=5, srid: int = None, dim=2, cache=False): -        if dim not in (2, 3): -            raise ValueError(_("Only 2 or 3 dimension")) -        if cache: -            coordinates = [self.cached_x, self.cached_y] -            if dim == 3: -                coordinates.append(self.cached_z) -        else: -            if self.x and self.y:  # user input -                if not srid or srid == self.spatial_reference_system.srid: -                    coordinates = [self.x, self.y] -                    if dim == 3: -                        coordinates.append(self.z) -                else: -                    args = { -                        "x": self.x, -                        "y": self.y, -                        "srid": self.spatial_reference_system.srid, -                    } -                    if dim == 3: -                        args["z"] = self.z -                    point = Point(**args).transform(srid, clone=True) -                    coordinates = [point.x, point.y] -                    if dim == 3: -                        coordinates.append(point.z) -            elif self.point_2d and dim == 2: -                point = self.point_2d.transform(srid, clone=True) -                coordinates = [point.x, point.y] -            elif self.point_3d and dim == 3: -                point = self.point_3d.transform(srid, clone=True) -                coordinates = [point.x, point.y, point.z] -            else: -                return -        if not rounded: -            return coordinates -        return [round(coord, rounded) for coord in coordinates] - -    def get_coordinates_from_polygon(self, rounded=5, srid: int = None): -        if self.multi_polygon: -            return self.convert_coordinates( -                self.multi_polygon.centroid, rounded=rounded, srid=srid -            ) - -    def get_x(self, srid: int = None) -> float: -        coord = self.get_coordinates(srid) -        if coord: -            return coord[0] - -    def get_y(self, srid: int = None) -> float: -        coord = self.get_coordinates(srid) -        if coord: -            return coord[1] - -    def get_z(self, srid: int = None) -> float: -        coord = self.get_coordinates(srid, dim=3) -        if coord: -            return coord[2] - -    @property -    def display_spatial_reference_system(self): -        profile = get_current_profile() -        if not profile.display_srs or not profile.display_srs.srid: -            return self.spatial_reference_system -        return profile.display_srs - -    def get_geo_items(self, get_polygons, rounded=5): -        label = self.label if hasattr(self, "label") else self.short_label -        dct = {"type": "Feature", "geometry": {}, "properties": {"label": label}} -        if get_polygons: -            list_coords = [] -            if self.multi_polygon: -                for polygon in self.multi_polygon: -                    list_coords.append([]) -                    for linear_ring in range(len(polygon)): -                        list_coords[-1].append([]) -                        for coords in polygon[linear_ring].coords: -                            point_2d = Point( -                                coords[0], coords[1], srid=self.multi_polygon.srid -                            ) -                            list_coords[-1][linear_ring].append( -                                self.convert_coordinates(point_2d, rounded) -                            ) -            dct["geometry"]["type"] = "MultiPolygon" -            dct["geometry"]["coordinates"] = list_coords -        else: -            dct["geometry"]["type"] = "Point" -            coords = self.display_coordinates() -            if coords: -                dct["geometry"]["coordinates"] = coords -            elif self.multi_polygon: -                dct["geometry"]["coordinates"] = self.convert_coordinates( -                    self.multi_polygon.centroid, rounded -                ) -            else: -                return {} -        return dct - -    def convert_coordinates(self, point_2d, rounded=5, srid=None): -        if not srid: -            profile = get_current_profile() -            if profile.display_srs and profile.display_srs.srid: -                srid = profile.display_srs.srid -        if not srid: -            x, y = point_2d.x, point_2d.y -        else: -            point = point_2d.transform(srid, clone=True) -            x, y = point.x, point.y -        if rounded: -            return [round(x, rounded), round(y, rounded)] -        return [x, y] - -    def most_precise_geo(self): -        if self.multi_polygon: -            return "multi_polygon" -        if self.point_2d: -            return "point" - -    def _geojson_serialize(self, geom_attr): -        if not hasattr(self, geom_attr): -            return "" -        geojson = serialize( -            "geojson", -            self.__class__.objects.filter(pk=self.pk), -            geometry_field=geom_attr, -            fields=("name",), -        ) -        geojson_dct = json.loads(geojson) -        profile = get_current_profile() -        precision = profile.point_precision - -        features = geojson_dct.pop("features") -        for idx in range(len(features)): -            feature = features[idx] -            lbl = feature["properties"].pop("name") -            feature["properties"]["name"] = lbl -            feature["properties"]["id"] = self.pk -            if precision is not None: -                geom_type = feature["geometry"].get("type", None) -                if geom_type == "Point": -                    feature["geometry"]["coordinates"] = [ -                        round(coord, precision) -                        for coord in feature["geometry"]["coordinates"] -                    ] -        geojson_dct["features"] = features -        geojson_dct["link_template"] = simple_link_to_window(self).replace( -            "999999", "<pk>" -        ) -        geojson = json.dumps(geojson_dct) -        return geojson - -    @property -    def point_2d_geojson(self): -        return self._geojson_serialize("point_2d") - -    @property -    def multi_polygon_geojson(self): -        return self._geojson_serialize("multi_polygon") - - - - -post_save.connect(post_save_geodata, sender=GeoVectorData) - - -class GeoItem(models.Model): -    main_geodata = models.ForeignKey( -        GeoVectorData, -        on_delete=models.SET_NULL, -        blank=True, -        null=True, -        related_name="main_related_items_%(app_label)s_%(class)s", -    ) -    geodata = models.ManyToManyField( -        GeoVectorData, blank=True, related_name="related_items_%(app_label)s_%(class)s" -    ) - +class GeoItem(GeographicItem):      GEO_SOURCE = (("T", _("Town")), ("P", _("Precise")), ("M", _("Polygon")))      # gis | 
