diff --git a/loko/assets/migrations/0031_naturetree_amenity_value_and_more.py b/loko/assets/migrations/0031_naturetree_amenity_value_and_more.py new file mode 100644 index 0000000..07f9885 --- /dev/null +++ b/loko/assets/migrations/0031_naturetree_amenity_value_and_more.py @@ -0,0 +1,108 @@ +# Generated by Django 6.0.7 on 2026-10-07 15:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0030_managementzone_managementzonedocument_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='naturetree', + name='amenity_value', + field=models.FloatField(blank=True, null=True, verbose_name="Valeur d'aménité (€)"), + ), + migrations.AddField( + model_name='naturetree', + name='biodiversity_index', + field=models.FloatField(blank=True, null=True, verbose_name='Indice de biodiversité'), + ), + migrations.AddField( + model_name='naturetree', + name='carbon_stock', + field=models.FloatField(blank=True, null=True, verbose_name='Stock de carbone (t CO2)'), + ), + migrations.AddField( + model_name='naturetree', + name='cooling_energy_indicator', + field=models.FloatField(blank=True, null=True, verbose_name='Énergie de refroidissement (kWh)'), + ), + migrations.AddField( + model_name='naturetree', + name='cooling_indicator', + field=models.FloatField(blank=True, null=True, verbose_name='Indice de rafraîchissement'), + ), + migrations.AddField( + model_name='naturetree', + name='cultivar', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Cultivar / Variété'), + ), + migrations.AddField( + model_name='naturetree', + name='death_date', + field=models.DateField(blank=True, null=True, verbose_name='Date de dépérissement / mort'), + ), + migrations.AddField( + model_name='naturetree', + name='genus', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Genre botanique'), + ), + migrations.AddField( + model_name='naturetree', + name='is_chronoxyle', + field=models.BooleanField(default=False, verbose_name='Arbre sénescent / Chronoxyle (bois mort sur pied)'), + ), + migrations.AddField( + model_name='naturetree', + name='is_dead', + field=models.BooleanField(default=False, verbose_name='Arbre mort'), + ), + migrations.AddField( + model_name='naturetree', + name='is_stump', + field=models.BooleanField(default=False, verbose_name='Souche'), + ), + migrations.AddField( + model_name='naturetree', + name='management_type', + field=models.CharField(blank=True, choices=[('municipal', 'Communal (Régie communale)'), ('regional', 'Régional (Bruxelles Mobilité / Région)'), ('community', 'Intercommunal / Autre pouvoir public'), ('private', 'Privé / Tiers')], default='municipal', max_length=50, null=True, verbose_name='Type de gestionnaire'), + ), + migrations.AddField( + model_name='naturetree', + name='managing_authority', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Autorité gestionnaire'), + ), + migrations.AddField( + model_name='naturetree', + name='number_of_trunks', + field=models.IntegerField(blank=True, default=1, null=True, verbose_name='Nombre de troncs / Cépée'), + ), + migrations.AddField( + model_name='naturetree', + name='plantation_coefficient', + field=models.FloatField(blank=True, null=True, verbose_name='Coefficient de plantation'), + ), + migrations.AddField( + model_name='naturetree', + name='planting_date', + field=models.DateField(blank=True, null=True, verbose_name='Date de plantation réelle'), + ), + migrations.AddField( + model_name='naturetree', + name='scientific_name', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Nom scientifique / latin'), + ), + migrations.AddField( + model_name='naturetree', + name='situation_coefficient', + field=models.FloatField(blank=True, null=True, verbose_name='Coefficient de situation'), + ), + migrations.AddField( + model_name='naturetree', + name='vernacular_name', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Nom vernaculaire / Essence'), + ), + ] diff --git a/loko/assets/models/nature.py b/loko/assets/models/nature.py index f65b28d..668d11c 100644 --- a/loko/assets/models/nature.py +++ b/loko/assets/models/nature.py @@ -146,6 +146,13 @@ TREE_PRUNING_CHOICES = [ ('none', _("Non taillé / Entretien minimal")), ] +TREE_MANAGEMENT_TYPE_CHOICES = [ + ('municipal', _("Communal (Régie communale)")), + ('regional', _("Régional (Bruxelles Mobilité / Région)")), + ('community', _("Intercommunal / Autre pouvoir public")), + ('private', _("Privé / Tiers")), +] + BULB_TYPE_CHOICES = [ ('tulipes', _('Tulipes')), @@ -224,6 +231,37 @@ class NatureTree(AbstractGeoAsset): pruning_frequency_years = models.IntegerField(null=True, blank=True, verbose_name=_("Périodicité de taille (années)")) last_pruning_date = models.DateField(null=True, blank=True, verbose_name=_("Dernière taille / élagage")) + # Gestionnaire & Compétence + managing_authority = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Autorité gestionnaire")) + management_type = models.CharField(max_length=50, choices=TREE_MANAGEMENT_TYPE_CHOICES, default='municipal', null=True, blank=True, verbose_name=_("Type de gestionnaire")) + + # Taxonomie & Botanique + vernacular_name = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Nom vernaculaire / Essence")) + scientific_name = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Nom scientifique / latin")) + genus = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Genre botanique")) + cultivar = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Cultivar / Variété")) + + # Dendrométrie & Morphologie complémentaire + number_of_trunks = models.IntegerField(null=True, blank=True, default=1, verbose_name=_("Nombre de troncs / Cépée")) + + # Éco-services & Valeur patrimoniale (Indicateurs écosystémiques) + amenity_value = models.FloatField(null=True, blank=True, verbose_name=_("Valeur d'aménité (€)")) + carbon_stock = models.FloatField(null=True, blank=True, verbose_name=_("Stock de carbone (t CO2)")) + cooling_indicator = models.FloatField(null=True, blank=True, verbose_name=_("Indice de rafraîchissement")) + cooling_energy_indicator = models.FloatField(null=True, blank=True, verbose_name=_("Énergie de refroidissement (kWh)")) + biodiversity_index = models.FloatField(null=True, blank=True, verbose_name=_("Indice de biodiversité")) + + # Évaluation patrimoniale & Barème + situation_coefficient = models.FloatField(null=True, blank=True, verbose_name=_("Coefficient de situation")) + plantation_coefficient = models.FloatField(null=True, blank=True, verbose_name=_("Coefficient de plantation")) + + # Statut biologique particulier + is_dead = models.BooleanField(default=False, verbose_name=_("Arbre mort")) + is_stump = models.BooleanField(default=False, verbose_name=_("Souche")) + is_chronoxyle = models.BooleanField(default=False, verbose_name=_("Arbre sénescent / Chronoxyle (bois mort sur pied)")) + death_date = models.DateField(null=True, blank=True, verbose_name=_("Date de dépérissement / mort")) + planting_date = models.DateField(null=True, blank=True, verbose_name=_("Date de plantation réelle")) + # Références des choix pour utilisation simple dans les templates DEVELOPMENT_STAGE_CHOICES = TREE_DEVELOPMENT_STAGE_CHOICES VITALITY_CHOICES = TREE_VITALITY_CHOICES @@ -233,6 +271,7 @@ class NatureTree(AbstractGeoAsset): PIT_TYPE_CHOICES = TREE_PIT_TYPE_CHOICES PROTECTION_CHOICES = TREE_PROTECTION_CHOICES PRUNING_CHOICES = TREE_PRUNING_CHOICES + MANAGEMENT_TYPE_CHOICES = TREE_MANAGEMENT_TYPE_CHOICES indexes = [ models.Index(fields=["code"]), diff --git a/loko/assets/templates/assets/nature_index.html b/loko/assets/templates/assets/nature_index.html index 55bd964..6dcb422 100644 --- a/loko/assets/templates/assets/nature_index.html +++ b/loko/assets/templates/assets/nature_index.html @@ -64,6 +64,16 @@ {% endfor %} +
+ + +
@@ -86,12 +96,15 @@ { field: 'highway', title: '{% translate "Ring/Autoroute" %}', align: 'center' } ]; - var assets_property_fields = ['category', 'model', 'area']; + var assets_property_fields = ['code', 'name', 'managing_authority', 'model', 'scientific_name', 'area']; var assets_columns = [ - { field: 'category', title: '{% translate "Catégorie" %}' }, + { field: 'code', title: '{% translate "Code" %}' }, + { field: 'name', title: '{% translate "Nom" %}' }, + { field: 'managing_authority', title: '{% translate "Gestionnaire" %}' }, { field: 'model', title: '{% translate "Modèle" %}' }, + { field: 'scientific_name', title: '{% translate "Botanique" %}' }, { field: 'area', title: '{% translate "Superficie" %}' } - ] + ]; const locations_layer_def = { @@ -161,16 +174,25 @@ 16, 5, 20, 8 ], - 'circle-color': ['coalesce', ['get', 'color'], '#0277BD'], + 'circle-color': [ + 'case', + ['==', ['get', 'management_type'], 'regional'], '#1E3A8A', + ['boolean', ['get', 'is_remarkable'], false], '#F59E0B', + ['coalesce', ['get', 'color'], '#15803D'] + ], 'circle-stroke-width': [ 'case', - ['boolean', ['feature-state', 'highlighted'], false], 2, + ['boolean', ['feature-state', 'highlighted'], false], 2.5, + ['==', ['get', 'management_type'], 'regional'], 1.5, + ['boolean', ['get', 'is_remarkable'], false], 2, 1 ], 'circle-stroke-color': [ 'case', - ['boolean', ['feature-state', 'highlighted'], false], '#ff0000', - '#000000' + ['boolean', ['feature-state', 'highlighted'], false], '#EF4444', + ['==', ['get', 'management_type'], 'regional'], '#93C5FD', + ['boolean', ['get', 'is_remarkable'], false], '#FEF08A', + '#064E3B' ], 'circle-opacity': 0.8 }, diff --git a/loko/assets/templates/assets/naturetree_detail.html b/loko/assets/templates/assets/naturetree_detail.html index 0ad5f8b..82f6a90 100644 --- a/loko/assets/templates/assets/naturetree_detail.html +++ b/loko/assets/templates/assets/naturetree_detail.html @@ -88,6 +88,24 @@ {% if asset.is_remarkable %} {% translate "Arbre remarquable" %} {% endif %} + + {# Badge Gestionnaire #} + {% if asset.managing_authority %} + + {{ asset.managing_authority }} + + {% elif asset.management_type %} + {{ asset.get_management_type_display }} + {% endif %} + + {# Badges Biologiques Spéciaux #} + {% if asset.is_dead %} + {% translate "Mort" %} + {% elif asset.is_stump %} + {% translate "Souche" %} + {% elif asset.is_chronoxyle %} + {% translate "Chronoxyle" %} + {% endif %}
@@ -159,6 +177,24 @@ {% if asset.is_remarkable %} {% translate "Arbre remarquable" %} {% endif %} + + {# Badge Gestionnaire #} + {% if asset.managing_authority %} + + {{ asset.managing_authority }} + + {% elif asset.management_type %} + {{ asset.get_management_type_display }} + {% endif %} + + {# Badges Biologiques Spéciaux #} + {% if asset.is_dead %} + {% translate "Mort" %} + {% elif asset.is_stump %} + {% translate "Souche" %} + {% elif asset.is_chronoxyle %} + {% translate "Chronoxyle" %} + {% endif %}
@@ -221,6 +257,89 @@ + + + {% translate "Autorité gestionnaire" %} + +
+ + {% if asset.managing_authority %} + {{ asset.managing_authority }} + {% if asset.management_type %} + ({{ asset.get_management_type_display }}) + {% endif %} + {% else %} + {{ asset.get_management_type_display|default:"-" }} + {% endif %} + +
+
+ +
+ + + + + + {% translate "Type de gestionnaire" %} + +
{{ asset.get_management_type_display|default:"-" }}
+
+ +
+ + + + + + {% translate "Nom scientifique / Latin" %} + +
+ + {{ asset.scientific_name|default:"-" }} + +
+
+ +
+ + + + + + {% translate "Nom vernaculaire / Essence" %} + +
{{ asset.vernacular_name|default:"-" }}
+
+ +
+ + + + + {% if asset.genus or asset.cultivar or 'genus' in allowed_update_fields %} + + {% translate "Genre & Cultivar" %} + +
+ + {{ asset.genus|default:"-" }}{% if asset.cultivar %} '{{ asset.cultivar }}'{% endif %} + +
+
+
+
+
+
+
+ + + {% endif %} + {% translate "Nom commun (FR)" %} @@ -419,6 +538,25 @@ + + + + + + @@ -518,6 +656,173 @@ +{# ── CARTE ÉCO-SERVICES & VALEUR PATRIMONIALE (GRALITY / BARÈME DE L'ARBRE) ── #} +
+
+
+ {% translate "Éco-services & Valeur patrimoniale" %} +
+ + {% translate "Indicateurs écologiques" %} + +
+
+ {# 4 mini-widgets écologiques #} +
+ +
+
+
+ {% translate "Aménité" %} +
+
+ {% if asset.amenity_value %} + {{ asset.amenity_value|floatformat:0 }} € + {% else %} + — + {% endif %} +
+
+
+ + +
+
+
+ {% translate "Carbone" %} +
+
+ {% if asset.carbon_stock %} + {{ asset.carbon_stock|floatformat:2 }} t CO₂ + {% else %} + — + {% endif %} +
+
+
+ + +
+
+
+ {% translate "Fraîcheur" %} +
+
+ {% if asset.cooling_indicator %} + {{ asset.cooling_indicator|floatformat:1 }} / 10 + {% else %} + — + {% endif %} +
+
+
+ + +
+
+
+ {% translate "Biodiversité" %} +
+
+ {% if asset.biodiversity_index %} + {{ asset.biodiversity_index|floatformat:2 }} + {% else %} + — + {% endif %} +
+
+
+
+ + {# Table détaillée des indicateurs écosystémiques #} +
{% translate "Nombre de troncs (cépée)" %} +
+ + {% if asset.number_of_trunks and asset.number_of_trunks > 1 %} + {{ asset.number_of_trunks }} {% translate "troncs" %} + {% else %} + 1 {% translate "(tronc unique)" %} + {% endif %} + +
+
+ +
+
{% translate "Hauteur totale (m)" %}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + {% if asset.death_date or asset.is_dead or 'death_date' in allowed_update_fields %} + + + + + {% endif %} + +
{% translate "Énergie de refroidissement (kWh)" %} +
+ + {% if asset.cooling_energy_indicator %} + {{ asset.cooling_energy_indicator|floatformat:1 }} kWh + {% else %} + - + {% endif %} + +
+
+ +
+
{% translate "Coefficient de situation (barème)" %} +
{{ asset.situation_coefficient|default:"-" }}
+
+ +
+
{% translate "Coefficient de plantation (barème)" %} +
{{ asset.plantation_coefficient|default:"-" }}
+
+ +
+
{% translate "Date de plantation constatée" %} +
+ + {% if asset.planting_date %} + {{ asset.planting_date|date:"d/m/Y" }} + {% else %} + - + {% endif %} + +
+
+ +
+
{% translate "Date de mort / dépérissement" %} +
+ + {% if asset.death_date %} + {{ asset.death_date|date:"d/m/Y" }} + {% else %} + - + {% endif %} + +
+
+ +
+
+
+ + + {# ── CARTE 3 : DIAGNOSTIC SANITAIRE & SÉCURITÉ ── #}
diff --git a/loko/assets/views/nature.py b/loko/assets/views/nature.py index e483ba6..915095b 100644 --- a/loko/assets/views/nature.py +++ b/loko/assets/views/nature.py @@ -71,6 +71,14 @@ def nature_index(request): categories.sort(key=lambda m: m.get_name().lower()) category_choices = [(m.id, m.get_name()) for m in categories] + selected_management_types = request.GET.getlist('management_type') + management_type_choices = [ + ('municipal', _("Communal (Commune)")), + ('regional', _("Régional (Bruxelles Mobilité)")), + ('community', _("Intercommunal / Communautaire")), + ('private', _("Privé / Conventionné")), + ] + models = list(NatureAssetModel.objects.all()) models.sort(key=lambda m: (m.get_name() or "").lower()) model_choices = [(m.id, m.get_name()) for m in models] @@ -92,6 +100,8 @@ def nature_index(request): 'model_choices': model_choices, 'category_model_map': dict(category_model_map), 'selected_thematic': selected_thematic, + 'management_type_choices': management_type_choices, + 'selected_management_types': selected_management_types, 'selected_statuses': statuses, 'selected_categories': selected_categories, 'selected_models': selected_models, @@ -551,7 +561,7 @@ def ensure_list(value): return [value] -MAX_ASSET_FEATURES = 5000 # limite imposée +MAX_ASSET_FEATURES = 10000 # limite imposée @check_thematic_access('nature') def nature_assets_geojson(request): @@ -570,6 +580,7 @@ def nature_assets_geojson(request): models = ensure_list(data.get('model')) highway = data.get('highway') == "1" location_type = data.get('location_type', '') + management_types = ensure_list(data.get('management_type')) # Récupération et parsing de la géométrie filter_geom_raw = data.get("filter_geom") @@ -590,11 +601,12 @@ def nature_assets_geojson(request): models = request.GET.getlist('model', []) highway = request.GET.get('highway') == "1" location_type = request.GET.get('location_type', '') + management_types = request.GET.getlist('management_type', []) filter_geom = None _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' - if not request.GET.get('unified') and not sections_ids and not categories and not models and not filter_geom and not _search_q and not highway and not location_type: # return empty geojson if no param with section_id + if not request.GET.get('unified') and not sections_ids and not categories and not models and not filter_geom and not _search_q and not highway and not location_type and not management_types: # return empty geojson if no param with section_id geojson_data = { "type": "FeatureCollection", "features": [], @@ -664,6 +676,11 @@ def nature_assets_geojson(request): "thematic": "nature", "is_location": False, "structure_type": obj.get('structure_type', ''), + "managing_authority": obj.get('managing_authority') or '', + "management_type": obj.get('management_type') or '', + "is_remarkable": obj.get('is_remarkable', False), + "scientific_name": obj.get('scientific_name') or '', + "vernacular_name": obj.get('vernacular_name') or '', } } @@ -715,6 +732,9 @@ def nature_assets_geojson(request): naturetree_qs = naturetree_qs.filter(model__id__in=models) natureshrub_qs = natureshrub_qs.filter(model__id__in=models) naturebench_qs = naturebench_qs.filter(model__id__in=models) + + if management_types: + naturetree_qs = naturetree_qs.filter(management_type__in=management_types) if categories: naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories) naturetree_qs = naturetree_qs.filter(category__id__in=categories) @@ -790,7 +810,9 @@ def nature_assets_geojson(request): .values( # on limite strictement les colonnes 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'category_name', 'model_name', 'color', 'geojson', - 'category_id', 'model_id', 'location_lon', 'location_lat' + 'category_id', 'model_id', 'location_lon', 'location_lat', + 'managing_authority', 'management_type', 'is_remarkable', + 'scientific_name', 'vernacular_name' ) .order_by('code') )[:MAX_ASSET_FEATURES + 1]