Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9dff4c2
data: add new sources
ludovicdmt Apr 1, 2025
e2935a0
data: update fibre
ludovicdmt Apr 1, 2025
98e4010
Data: fix LCZ geom + simpler MVT generation
ludovicdmt Apr 2, 2025
8ce5113
fix
ludovicdmt Apr 2, 2025
ee466d0
data: force to to not possible to plant where one blocking factor
ludovicdmt Apr 2, 2025
e8a9328
data: adjust color scale and text
ludovicdmt Apr 3, 2025
7d0c80c
fix: cypress tests
ludovicdmt Apr 3, 2025
aa64ad9
fix
ludovicdmt Apr 3, 2025
4a5e6bc
tests(front): less MVT to be faster
ludovicdmt Apr 3, 2025
b2246dc
Revert "tests(front): less MVT to be faster"
ludovicdmt Apr 3, 2025
c41fec0
data: new thresholds for plantability tiles
ludovicdmt Apr 3, 2025
4b194a9
chore: update deprecated function
ludovicdmt Apr 4, 2025
34f42d9
chore(back): refacto utils
ludovicdmt Apr 4, 2025
7fdf2f7
chore(back): comments
ludovicdmt Apr 4, 2025
7f051f5
test(doc): add tests for data processing during import
ludovicdmt Apr 4, 2025
99722dc
doc(back): Update new data source
ludovicdmt Apr 4, 2025
5875162
Merge branch 'dev' into data-new-factors
ludovicdmt Apr 4, 2025
58f4bab
fix(back, front): remove plantability thresholding in front and move …
ludovicdmt Apr 4, 2025
5751804
test(front): use text from enum
ludovicdmt Apr 4, 2025
1e70bf0
doc(back): update mkdocs for utils
ludovicdmt Apr 4, 2025
ebd4992
chore: lint
ludovicdmt Apr 4, 2025
2769f38
fix
ludovicdmt Apr 6, 2025
9453bbf
chore: move constants
ludovicdmt Apr 7, 2025
2d49058
changelog: ajout des changements
ludovicdmt Apr 8, 2025
67a7c27
changelog update
ludovicdmt Apr 8, 2025
edca9cf
Revert DRF changelog
ludovicdmt Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion back/api/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db.models import TextChoices

DEFAULT_ZOOM_LEVELS = (12, 16)
DEFAULT_ZOOM_LEVELS = (11, 18)


class GeoLevel(TextChoices):
Expand Down
148 changes: 49 additions & 99 deletions back/api/utils/mvt_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
MVT Generator as django-media.
"""

import math
from concurrent.futures import ThreadPoolExecutor, as_completed
import gc

from django.contrib.gis.db.models import Extent
from django.contrib.gis.geos import Polygon, MultiPolygon
from django.contrib.gis.geos import Polygon
from django.db.models import QuerySet
from shapely import Polygon as Polygon_shapely
from django.contrib.gis.db.models.functions import Intersection
import mercantile
import mapbox_vector_tile
from typing import List, Dict, Any, Tuple
from typing import Dict
from iarbre_data.models import MVTTile
from tqdm import tqdm

Expand Down Expand Up @@ -64,6 +64,7 @@ def generate_tiles(self, ignore_existing=False):
truncate=True,
)
)

with ThreadPoolExecutor(max_workers=self.number_of_thread) as executor:
future_to_tiles = {
executor.submit(self._generate_tile_for_zoom, tile, zoom): tile
Expand Down Expand Up @@ -112,116 +113,65 @@ def _generate_tile_for_zoom(self, tile: mercantile.Tile, zoom: int) -> None:

Returns:
None

Reference:
https://makina-corpus.com/django/generer-des-tuiles-vectorielles-sur-mesure-avec-django
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai l'impression que tu as grandement simplifié le code, sans ajouter de nouvelle dépendance.

Est-ce que tu as aussi constaté eu des gains de temps de calcul ?
(c'est cool niveau comm de dire qu'on a eu des améliorations de perfs !)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non pas de gain, ni de différence. Juste maintenant c'est mapbox_vector_tile qui fait les opérations au lieu que ce soit nous à la main.

"""
# Calculate tile bounds
tile_bounds = mercantile.xy_bounds(tile)
west, south, east, north = tile_bounds
pixel = self.pixel_length(zoom)
buffer = 4 * pixel

# Create GeoDjango polygon for tile extent
tile_polygon = Polygon.from_bbox((west, south, east, north))
tile_polygon = Polygon.from_bbox(
(west - buffer, south - buffer, east + buffer, north + buffer)
)
tile_polygon.srid = TARGET_MAP_PROJ
# Filter queryset to tile extent and then clip it
clipped_queryset = self.queryset.filter(
map_geometry__intersects=tile_polygon
).annotate(clipped_geometry=Intersection("map_geometry", tile_polygon))

if not clipped_queryset.exists():
return

# Prepare MVT features
features = self._prepare_mvt_features(clipped_queryset, tile_polygon)
if not features:
return

# Encode MVT
mvt_data = mapbox_vector_tile.encode(
[
{
"name": f"{self.geolevel}--{self.datatype}",
"features": features,
}
]
)
filename = f"{self.geolevel}/{self.datatype}/{zoom}/{tile.x}/{tile.y}.mvt"
mvt_tile = MVTTile(
geolevel=self.geolevel,
datatype=self.datatype,
zoom_level=zoom,
tile_x=tile.x,
tile_y=tile.y,
)
mvt_tile.save_mvt(mvt_data, filename)

@staticmethod
def _prepare_mvt_features(
queryset: QuerySet, tile_polygon: Polygon
) -> List[Dict[str, Any]]:
"""
Prepare features for MVT encoding.

Args:
queryset (QuerySet): Queryset of the model.
tile_polygon (Polygon): Polygon of the tile extent.

Returns:
List of features to encode in MVT format.
"""
features = []
(x0, y0, x_max, y_max) = tile_polygon.extent
x_span = x_max - x0
y_span = y_max - y0
for obj in tqdm(queryset, desc="Preparing MVT features", total=len(queryset)):
clipped_geom = obj.clipped_geometry
if not clipped_geom.empty:
if isinstance(clipped_geom, MultiPolygon):
clipped_geom_list = list(clipped_geom)
elif isinstance(clipped_geom, Polygon):
clipped_geom_list = [clipped_geom]
else:
raise TypeError("clipped_geom is not MultiPolygon or a Polygon.")
for clipped in clipped_geom_list:
tile_based_coords = MVTGenerator.transform_to_tile_relative(
clipped, x0, y0, x_span, y_span
)
feature = {
"geometry": Polygon_shapely(tile_based_coords),
"properties": obj.get_layer_properties(),
if clipped_queryset.exists():
transformed_geometries = {
"name": f"{self.geolevel}--{self.datatype}",
"features": [],
}

for obj in tqdm(
clipped_queryset,
desc=f"Processing MVT Tile: ({tile.x}, {tile.y}, {zoom})",
):
properties = obj.get_layer_properties()
clipped_geom = obj.clipped_geometry
transformed_geometries["features"].append(
{
"geometry": clipped_geom.make_valid()
.simplify(pixel, preserve_topology=True)
.wkt,
"properties": properties,
}
features.append(feature)

return features

@staticmethod
def transform_to_tile_relative(
clipped_geom: Polygon, x0: float, y0: float, x_span: float, y_span: float
) -> List[Tuple[int, int]]:
"""
Transform coordinates to relative coordinates within the MVT tile.

Args:
clipped_geom (Polygon): The clipped geometry object.
x0 (float): The minimum x-coordinate of the tile extent.
y0 (float): The minimum y-coordinate of the tile extent.
x_span (float): The span of the x-coordinates of the tile extent.
y_span (float): The span of the y-coordinates of the tile extent.

Returns:
List[Tuple[int, int]]: List of transformed coordinates relative to the MVT tile.
"""
tile_based_coords = []

coords = clipped_geom.coords[0]
)

if (
len(clipped_geom.coords) == 2
): # Some geometry have too many points and tuple is length 2
coords = coords + clipped_geom.coords[1]
mvt_data = mapbox_vector_tile.encode(
transformed_geometries, quantize_bounds=(west, south, east, north)
)

for x_merc, y_merc in coords:
tile_based_coord = (
int((x_merc - x0) * MVT_EXTENT / x_span),
int((y_merc - y0) * MVT_EXTENT / y_span),
filename = f"{self.geolevel}/{self.datatype}/{zoom}/{tile.x}/{tile.y}.mvt"
mvt_tile = MVTTile(
geolevel=self.geolevel,
datatype=self.datatype,
zoom_level=zoom,
tile_x=tile.x,
tile_y=tile.y,
)
tile_based_coords.append(tile_based_coord)
mvt_tile.save_mvt(mvt_data, filename)

return tile_based_coords
@staticmethod
def pixel_length(zoom):
"""Width of a pixel in Web Mercator"""
RADIUS = 6378137
CIRCUM = 2 * math.pi * RADIUS
SIZE = 512
return CIRCUM / SIZE / 2 ** int(zoom)
2 changes: 1 addition & 1 deletion back/compute_factors.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Configuration
input_file="insee.txt" # File containing the INSEE codes
task_command="python manage.py c04_compute_factors --delete --insee_code_city" # Base task command
num_parallel_tasks=4 # Number of parallel tasks
num_parallel_tasks=4 # Number of parallel tasks

# Function to process INSEE codes
process_insee_code() {
Expand Down
93 changes: 85 additions & 8 deletions back/iarbre_data/data_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
},
{
"name": "SLT",
"file": "sltmateriel.geojson",
"file": "voirie.gpkg",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Est-ce le bon nom de fichier ? Dans ce cas, pourquoi avoir également ajouté (en commentaire) un autre fichier voirie.cpkg?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oui c'est le bon nom. gpkg c'est un fichier qui contient plusieurs layers (quand geojson) n'en a qu'une.
Valérian des SIG à la voirie m'a fait un export en gpkg de toutes les données les concernant, quand la dernière fois ils avaient fait des exports séparés en geojson.
Il a juste oublié un champ indispensable pour les données SLT donc je garde l'ancien fichier en attendant qu'il me refasse l'export et que je puisse switcher sur la version commentée (j'ai pas envie de ré-écrire la même chose dans 1 semaine donc je stocke là temporairement en commentaire).

"scripts": ["slt.py"],
"layer_name": "pub_patrimoinevoirie.sltmateriel",
"actions": [
{"buffer_size": 2, "union": True},
{"buffer_size": 2, "union": False},
],
"factors": ["Signalisation tricolore et lumineuse matériel"],
"output_type": "POINT",
Expand All @@ -20,10 +21,55 @@
"name": "Assainissement",
"file": "assainissement.geojson",
"scripts": ["assainissement.py"],
"actions": [{"buffer_size": 1, "explode": True, "union": True}],
"actions": [{"buffer_size": 1, "explode": True, "union": False}],
"factors": ["Assainissement"],
"output_type": "MULTILINESTRING",
},
# {
# "name": "Espaces publics",
# "file": "voirie.gpkg",
# "layer_name": "pub_patrimoinevoirie.espacepublic",
# "actions": [
# {
# "filter": {
# "name": "typeespacepublic",
# "value": "Parc / jardin public / square",
# }
# },
# {
# "filter": {
# "name": "typeespacepublic",
# "value": "Giratoire",
# }
# },
# {
# "filters": [
# {
# "name": "typeespacepublic",
# "value": "Aire de jeux",
# },
# {
# "name": "typeespacepublic",
# "value": "Espace piétonnier",
# },
# ]
# },
# {
# "filter": {
# "name": "typeespacepublic",
# "value": "Délaissé / Ilot végétalisé",
# }
# },
# ],
# "scripts": ["parc.py", "giratoire.py", "jeux.py" "friche_nat.py"],
# "factors": [
# "Parcs et jardins publics",
# "Giratoires",
# "Espaces jeux et pietonnier",
# "Friche naturelle",
# ],
# "output_type": "POLYGON",
# },
{
"name": "Espaces publics",
"file": "espacepublic.geojson",
Expand Down Expand Up @@ -152,27 +198,43 @@
{
"name": "Réseaux gaz",
"file": "rsx_gaz.geojson",
"actions": [{"buffer_size": 2, "union": True}],
"actions": [{"buffer_size": 2, "union": False}],
"scripts": ["gaz.py"],
"factors": ["Rsx gaz"],
"output_type": "LINESTRING",
},
{
"name": "Réseaux souterrains Enedis",
"file": "rsx_souterrain_enedis.geojson",
"actions": [{"buffer_size": 2, "union": True}],
"actions": [{"buffer_size": 2, "union": False}],
"scripts": ["souterrain_enedis.py"],
"factors": ["Rsx souterrains ERDF"],
"output_type": "LINESTRING",
},
{
"name": "Réseaux aériens Enedis",
"file": "rsx_aerien_enedis.geojson",
"actions": [{"buffer_size": 1, "union": True}],
"actions": [{"buffer_size": 1, "union": False}],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pourquoi ce passage de True à False. Est-ce qu'il y avait un bug avant ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non avant j'explosais les géométries en suivant une grille régulière pour simplifier les traitements suivants en me permettant de travailler avec juste des bouts de géom.
Je me suis rendu compte qu'en gardant juste l'explosion inhérente aux données (donc pas d'union), ça va tout aussi vite pour les traitements suivants mais ça simplifie l'import des données.

"scripts": ["aerien_enedis.py"],
"factors": ["Rsx aériens ERDF"],
"output_type": "LINESTRING",
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fichier data.md à mettre à jour non ?

"name": "Place PMR",
"file": "voirie.gpkg",
"layer_name": "pub_patrimoinevoirie.emplacementpmr",
"actions": [{"buffer_size": 3}],
"factors": ["PMR"],
"output_type": "POLYGON",
},
{
"name": "Autopartage",
"file": "voirie.gpkg",
"layer_name": "pub_patrimoinevoirie.stationautopartage",
"actions": [{"buffer_size": 3}],
"factors": ["Autopartage"],
"output_type": "POLYGON",
},
]
URL_FILES = [
{
Expand All @@ -183,7 +245,7 @@
"outputFormat=GML3&SRSNAME=EPSG:2154&startIndex=0&sortBy=gid",
"scripts": ["fibre.py"],
"layer_name": "tel_telecom.telfibreripthd_1",
"actions": [{"buffer_size": 2, "union": True}],
"actions": [{"buffer_size": 2, "union": False}],
"factors": ["Réseau Fibre"],
"output_type": "LINESTRING",
},
Expand Down Expand Up @@ -420,7 +482,7 @@
"Marchés forains": 1,
"Pistes cyclable": -1,
"Plan eau": -5, # -3
"Ponts": -3,
"Ponts": -5,
"Réseau de chaleur urbain": -3,
"Voies ferrées": -5, # -2
"Strate arborée": 1,
Expand All @@ -434,9 +496,24 @@
"Rsx gaz": -3,
"Rsx souterrains ERDF": -1,
"Rsx aériens ERDF": -2,
"PMR": -4,
"Auto-partage": -2,
# "QPV": 1,
}

UPDATES = [
{
"name": "Réseau Fibre",
"file": "voirie.gpkg",
"layer_name": "pub_app_patrimoinevoirie.liengeniecivil",
"actions": [
{"buffer_size": 2, "union": False},
],
"factors": ["Fibre"],
"output_type": "POINT",
},
]

LCZ = {
"1": "Ensemble compact de tours",
"2": "Ensemble compact d'immeubles",
Expand Down
Loading