Skip to content

Commit

Permalink
Merge pull request #209 from MTES-MCT/new_faq
Browse files Browse the repository at this point in the history
Création d'une abrorescence pour la FAQ
  • Loading branch information
thibault committed Jun 30, 2023
2 parents dd05ff6 + 16aea21 commit d2bdcd5
Show file tree
Hide file tree
Showing 35 changed files with 1,074 additions and 232 deletions.
18 changes: 15 additions & 3 deletions envergo/geodata/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from envergo.geodata.forms import DepartmentForm
from envergo.geodata.models import Department, Map, Parcel, Zone
from envergo.geodata.tasks import process_shapefile_map
from envergo.geodata.tasks import generate_map_preview, process_shapefile_map
from envergo.geodata.utils import count_features, extract_shapefile


Expand Down Expand Up @@ -60,7 +60,7 @@ def queryset(self, request, queryset):


@admin.register(Map)
class MapAdmin(admin.ModelAdmin):
class MapAdmin(gis_admin.GISModelAdmin):
form = MapForm
list_display = [
"name",
Expand All @@ -80,7 +80,7 @@ class MapAdmin(admin.ModelAdmin):
"task_status",
"import_error_msg",
]
actions = ["process"]
actions = ["process", "generate_preview"]
exclude = ["task_id"]
search_fields = ["name", "display_name"]
list_filter = ["import_status", "map_type", "data_type", DepartmentsListFilter]
Expand Down Expand Up @@ -161,6 +161,18 @@ def process(self, request, queryset):
)
self.message_user(request, msg, level=messages.INFO)

@admin.action(description=_("Generate the simplified preview geometry"))
def generate_preview(self, request, queryset):
if queryset.count() > 1:
error = _("Please only select one map for this action.")
self.message_user(request, error, level=messages.ERROR)
return

map = queryset[0]
generate_map_preview.delay(map.id)
msg = _("The map preview will be updated soon.")
self.message_user(request, msg, level=messages.INFO)

@admin.display(description=_("Extracted zones"))
def zone_count(self, obj):
count = Zone.objects.filter(map=obj).count()
Expand Down
20 changes: 20 additions & 0 deletions envergo/geodata/migrations/0003_map_geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2 on 2023-06-16 12:36

import django.contrib.gis.db.models.fields
from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("geodata", "0002_map_imported_zones"),
]

operations = [
migrations.AddField(
model_name="map",
name="geometry",
field=django.contrib.gis.db.models.fields.MultiPolygonField(
geography=True, null=True, srid=4326, verbose_name="Simplified geometry"
),
),
]
4 changes: 3 additions & 1 deletion envergo/geodata/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ class Map(models.Model):
choices=DEPARTMENT_CHOICES,
),
)

geometry = gis_models.MultiPolygonField(
_("Simplified geometry"), geography=True, null=True
)
created_at = models.DateTimeField(_("Date created"), default=timezone.now)
expected_zones = models.IntegerField(_("Expected zones"), default=0)
imported_zones = models.IntegerField(_("Imported zones"), null=True, blank=True)
Expand Down
20 changes: 18 additions & 2 deletions envergo/geodata/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,36 @@

from config.celery_app import app
from envergo.geodata.models import STATUSES, Map
from envergo.geodata.utils import process_shapefile
from envergo.geodata.utils import process_shapefile, simplify_map

logger = logging.getLogger(__name__)


@app.task(bind=True)
@transaction.atomic
def process_shapefile_map(task, map_id):
logger.info(f"Starting import on map {map_id}")

map = Map.objects.get(pk=map_id)

# Store the task data in the model, so we can display progression
# in the admin page.
map.task_id = task.request.id
map.import_error_msg = ""
map.import_status = None
map.save()

map.zones.all().delete()
# Proceed with the map import
try:
with transaction.atomic():
map.zones.all().delete()
process_shapefile(map, map.file, task)
map.geometry = simplify_map(map)
except Exception as e:
map.import_error_msg = f"Erreur d'import ({e})"
logger.error(map.import_error_msg)

# Update the map status and metadata
nb_imported_zones = map.zones.all().count()
if map.expected_zones == nb_imported_zones:
map.import_status = STATUSES.success
Expand All @@ -38,3 +45,12 @@ def process_shapefile_map(task, map_id):
map.task_id = None
map.imported_zones = nb_imported_zones
map.save()


@app.task(bind=True)
def generate_map_preview(task, map_id):
logger.info(f"Starting preview generation on map {map_id}")

map = Map.objects.get(pk=map_id)
map.geometry = simplify_map(map)
map.save()
53 changes: 50 additions & 3 deletions envergo/geodata/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@

import requests
from django.contrib.gis.gdal import DataSource
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Polygon
from django.contrib.gis.utils.layermapping import LayerMapping
from django.core.serializers import serialize
from django.db import connection
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _

from envergo.geodata.models import Zone

logger = logging.getLogger(__name__)

EPSG_WGS84 = 4326


class CeleryDebugStream:
"""A sys.stdout proxy that also updates the celery task states.
Expand Down Expand Up @@ -49,6 +52,8 @@ def write(self, msg):


class CustomMapping(LayerMapping):
"""A custom LayerMapping that allows to pass extra arguments to the generated model."""

def __init__(self, *args, **kwargs):
self.extra_kwargs = kwargs.pop("extra_kwargs")
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -127,8 +132,6 @@ def to_geojson(obj, geometry_field="geometry"):
srid.
"""

EPSG_WGS84 = 4326

if isinstance(obj, (QuerySet, list)):
geojson = serialize("geojson", obj, geometry_field=geometry_field)
elif hasattr(obj, "geojson"):
Expand Down Expand Up @@ -191,3 +194,47 @@ def merge_geometries(polygons):
pass

return merged


def simplify_map(map):
"""Generates a simplified geometry for the entire map.
This methods takes a map and generates a single polygon that is the union
of all the polygons in the map.
We also simplify the polygon because this is for display purpose only.
We use native postgis methods those operations, because it's way faster.
As for simplification, we don't preserve topology (ST_Simplify instead of
ST_SimplifyPreserveTopology) because we want to be able to drop small
holes in the polygon.
Because of that, we also have to call ST_MakeValid to avoid returning invalid
polygons."""

with connection.cursor() as cursor:
cursor.execute(
"""
SELECT
ST_AsText(
ST_MakeValid(
ST_Simplify(
ST_Union(z.geometry::geometry),
0.0001
),
'method=structure keepcollapsed=false'
)
)
AS polygon
FROM geodata_zone as z
WHERE z.map_id = %s
""",
[map.id],
)
row = cursor.fetchone()

polygon = GEOSGeometry(row[0], srid=EPSG_WGS84)
if isinstance(polygon, Polygon):
polygon = MultiPolygon(polygon)
return polygon
17 changes: 17 additions & 0 deletions envergo/moulinette/migrations/0005_contact_regulation_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2 on 2023-06-22 12:40

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("moulinette", "0004_alter_contact_options"),
]

operations = [
migrations.AddField(
model_name="contact",
name="regulation_url",
field=models.URLField(blank=True, verbose_name="Regulation URL"),
),
]
12 changes: 9 additions & 3 deletions envergo/moulinette/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance as D
from django.db import models
from django.db.models import F
from django.db.models import Case, F, When
from django.db.models.functions import Cast
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -158,7 +158,7 @@ def __init__(self, data, raw_data):
self.catalog["config"] = self.department.moulinette_config

self.perimeters = self.get_perimeters()
self.criterions = self.get_criterions()
self.criterions_classes = self.get_criterions()

# This is a clear case of circular references, since the Moulinette
# holds references to the regulations it's computing, but regulations and
Expand Down Expand Up @@ -251,7 +251,12 @@ def get_perimeters(self):
Perimeter.objects.filter(
map__zones__geometry__dwithin=(coords, F("activation_distance"))
)
.annotate(geometry=F("map__zones__geometry"))
.annotate(
geometry=Case(
When(map__geometry__isnull=False, then=F("map__geometry")),
default=F("map__zones__geometry"),
)
)
.annotate(distance=Distance("map__zones__geometry", coords))
.order_by("distance", "map__name")
.select_related("map", "contact")
Expand Down Expand Up @@ -449,6 +454,7 @@ class Contact(models.Model):
)
name = models.CharField(_("Name"), max_length=256)
url = models.URLField(_("URL"), blank=True)
regulation_url = models.URLField(_("Regulation URL"), blank=True)
address_md = models.TextField(_("Address"))
address_html = models.TextField(_("Address HTML"), blank=True)

Expand Down
28 changes: 20 additions & 8 deletions envergo/moulinette/regulations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from abc import ABC
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
Expand All @@ -21,18 +22,24 @@ def __str__(self):
return self.text


class MoulinetteRegulation:
"""Run the moulinette for a single regulation (e.g Loi sur l'eau)."""
class MoulinetteRegulation(ABC):
"""Run the moulinette for a single regulation (e.g Loi sur l'eau).
This class is meant to be inherited to implement actual regulations.
"""

# Implement this in subclasses
criterion_classes = []

def __init__(self, moulinette):
self.moulinette = moulinette
self.moulinette.catalog.update(self.get_catalog_data())

# Instanciate the criterions
self.criterions = [
Criterion(moulinette)
for Criterion in self.criterion_classes
if Criterion in moulinette.criterions
perimeter.criterion(moulinette, perimeter)
for perimeter in moulinette.perimeters
if perimeter.criterion in self.criterion_classes
]

def get_catalog_data(self):
Expand Down Expand Up @@ -119,7 +126,11 @@ def _get_map(self):

@dataclass
class MapPolygon:
"""Data that can be displayed and labeled on a leaflet map as a polygon."""
"""Data that can be displayed and labeled on a leaflet map as a polygon.
A `MapPolygon is meant to represent a single entry on a map:
a polygon with a given color and label.
"""

perimeters: list # List of `envergo.geofr.Perimeter` objects
color: str
Expand Down Expand Up @@ -185,7 +196,7 @@ def sources(self):
return maps


class MoulinetteCriterion:
class MoulinetteCriterion(ABC):
"""Run a single moulinette check."""

# Prevent template engine to instanciate the class since we sometimes want
Expand All @@ -198,9 +209,10 @@ class MoulinetteCriterion:
# "Nomenclature réglementations & critères" document.
CODES = ["soumis", "non_soumis", "action_requise", "non_concerne"]

def __init__(self, moulinette):
def __init__(self, moulinette, perimeter):
self.moulinette = moulinette
self.moulinette.catalog.update(self.get_catalog_data())
self.perimeter = perimeter

def get_catalog_data(self):
"""Get data to inject to the global catalog."""
Expand Down
27 changes: 27 additions & 0 deletions envergo/moulinette/regulations/sage.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,30 @@ def result(self):
result = RESULTS.non_disponible

return result

def _get_map(self):
# Let's find the first map that we can display
perimeter = next(
(
criterion.perimeter
for criterion in self.criterions
if criterion.perimeter.map.display_for_user
and criterion.perimeter.map.geometry
),
None,
)
if not perimeter:
return None

map_polygons = [MapPolygon([perimeter], "red", "Sage")]
caption = "Le projet se situe dans le périmètre du Sage."

map = Map(
center=self.catalog["coords"],
entries=map_polygons,
caption=caption,
truncate=False,
zoom=None,
)

return map
10 changes: 10 additions & 0 deletions envergo/pages/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib import admin

from envergo.pages.models import NewsItem


@admin.register(NewsItem)
class NewsItemAdmin(admin.ModelAdmin):
list_display = ["title", "created_at"]
search_fields = ["title", "content_md"]
fields = ["title", "content_md", "created_at"]
Loading

0 comments on commit d2bdcd5

Please sign in to comment.