feat(assets): add generic ManagementZone model, admin, geojson API and map overlay
This commit is contained in:
parent
8807531eb3
commit
d36649f3bf
34 changed files with 1553 additions and 7 deletions
|
|
@ -745,4 +745,59 @@ class ParkingSpotAdmin(AssetGISAdmin):
|
|||
)
|
||||
search_fields = ("code", "name_fr", "name_nl", "note")
|
||||
filter_horizontal = ("poles", "panels")
|
||||
raw_id_fields = ("location", "street")
|
||||
raw_id_fields = ("location", "street")
|
||||
|
||||
# ─── Management Zones ──────────────────────────────────────────────────────────
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.contrib.contenttypes.admin import GenericTabularInline
|
||||
from .models import ManagementZoneType, ManagementZone, ManagementZoneAsset, ManagementZoneDocument
|
||||
|
||||
|
||||
class ManagementZoneDocumentInline(admin.TabularInline):
|
||||
model = ManagementZoneDocument
|
||||
extra = 1
|
||||
|
||||
|
||||
class ManagementZoneAssetInline(GenericTabularInline):
|
||||
model = ManagementZoneAsset
|
||||
ct_field = 'content_type'
|
||||
ct_fk_field = 'object_id'
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(ManagementZoneType)
|
||||
class ManagementZoneTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('code', 'name_fr', 'name_nl', 'thematic', 'color_badge')
|
||||
list_filter = ('thematic',)
|
||||
search_fields = ('code', 'name_fr', 'name_nl', 'description')
|
||||
|
||||
def color_badge(self, obj):
|
||||
color = obj.default_color or "#198754"
|
||||
return format_html(
|
||||
'<span style="display:inline-block;width:16px;height:16px;background-color:{};border:1px solid #999;border-radius:3px;vertical-align:middle;margin-right:6px;"></span>{}',
|
||||
color, color
|
||||
)
|
||||
color_badge.short_description = "Couleur"
|
||||
|
||||
|
||||
@admin.register(ManagementZone)
|
||||
class ManagementZoneAdmin(AssetGISAdmin):
|
||||
list_display = ('code', 'name_fr', 'thematic', 'zone_type', 'color_badge', 'target_frequency_days', 'is_active', 'has_geom')
|
||||
list_filter = ('thematic', 'zone_type', 'is_active')
|
||||
search_fields = ('code', 'name_fr', 'name_nl', 'instructions', 'description', 'seasonality_notes')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
inlines = [ManagementZoneDocumentInline, ManagementZoneAssetInline]
|
||||
|
||||
def color_badge(self, obj):
|
||||
color = obj.get_color()
|
||||
return format_html(
|
||||
'<span style="display:inline-block;width:16px;height:16px;background-color:{};border:1px solid #999;border-radius:3px;vertical-align:middle;margin-right:6px;"></span>{}',
|
||||
color, color
|
||||
)
|
||||
color_badge.short_description = "Couleur"
|
||||
|
||||
def has_geom(self, obj):
|
||||
return bool(obj.geom)
|
||||
has_geom.boolean = True
|
||||
has_geom.short_description = "Géométrie"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8710,3 +8710,57 @@ msgstr "Assets directly linked to building"
|
|||
|
||||
#~ msgid "Arceaux cargo constatés"
|
||||
#~ msgstr "Observed cargo bike racks"
|
||||
|
||||
msgid "Type de zone de gestion"
|
||||
msgstr "Management zone type"
|
||||
|
||||
msgid "Types de zones de gestion"
|
||||
msgstr "Management zone types"
|
||||
|
||||
msgid "Zone de gestion"
|
||||
msgstr "Management zone"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Management zones"
|
||||
|
||||
msgid "Consignes et points d'attention"
|
||||
msgstr "Instructions and points of attention"
|
||||
|
||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
msgstr "Operational instructions, points of vigilance, safety rules..."
|
||||
|
||||
msgid "Périmètre géographique"
|
||||
msgstr "Geographical perimeter"
|
||||
|
||||
msgid "Couleur d'affichage"
|
||||
msgstr "Display color"
|
||||
|
||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
msgstr "Hexadecimal code (e.g. #198754). If empty, the zone type color is used."
|
||||
|
||||
msgid "Fréquence cible (jours)"
|
||||
msgstr "Target frequency (days)"
|
||||
|
||||
msgid "Intervalle indicatif entre interventions en jours."
|
||||
msgstr "Indicative interval between interventions in days."
|
||||
|
||||
msgid "Saisonnalité / Calendrier"
|
||||
msgstr "Seasonality / Calendar"
|
||||
|
||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
msgstr "E.g.: Mowing from April to October; late mowing in September; pruning prohibited in spring."
|
||||
|
||||
msgid "Asset de zone de gestion"
|
||||
msgstr "Management zone asset"
|
||||
|
||||
msgid "Assets de zones de gestion"
|
||||
msgstr "Management zone assets"
|
||||
|
||||
msgid "Document de zone de gestion"
|
||||
msgstr "Management zone document"
|
||||
|
||||
msgid "Documents de zones de gestion"
|
||||
msgstr "Management zone documents"
|
||||
|
||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
msgstr "If checked, this asset is excluded from the zone even if its geometry intersects the perimeter."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -717,3 +717,30 @@ msgstr "Validation error."
|
|||
msgid "Erreur réseau."
|
||||
msgstr "Network error."
|
||||
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Management zones"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "No management zone defined for this thematic."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Show / hide all zones"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Opacity"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Target frequency:"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "days"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Instructions & points of attention"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Center on zone"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8763,3 +8763,57 @@ msgstr "Assets rattachés directement au bâtiment"
|
|||
|
||||
#~ msgid "Arceaux cargo constatés"
|
||||
#~ msgstr "Arceaux cargo constatés"
|
||||
|
||||
msgid "Type de zone de gestion"
|
||||
msgstr "Type de zone de gestion"
|
||||
|
||||
msgid "Types de zones de gestion"
|
||||
msgstr "Types de zones de gestion"
|
||||
|
||||
msgid "Zone de gestion"
|
||||
msgstr "Zone de gestion"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Zones de gestion"
|
||||
|
||||
msgid "Consignes et points d'attention"
|
||||
msgstr "Consignes et points d'attention"
|
||||
|
||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
msgstr "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
|
||||
msgid "Périmètre géographique"
|
||||
msgstr "Périmètre géographique"
|
||||
|
||||
msgid "Couleur d'affichage"
|
||||
msgstr "Couleur d'affichage"
|
||||
|
||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
msgstr "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
|
||||
msgid "Fréquence cible (jours)"
|
||||
msgstr "Fréquence cible (jours)"
|
||||
|
||||
msgid "Intervalle indicatif entre interventions en jours."
|
||||
msgstr "Intervalle indicatif entre interventions en jours."
|
||||
|
||||
msgid "Saisonnalité / Calendrier"
|
||||
msgstr "Saisonnalité / Calendrier"
|
||||
|
||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
msgstr "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
|
||||
msgid "Asset de zone de gestion"
|
||||
msgstr "Asset de zone de gestion"
|
||||
|
||||
msgid "Assets de zones de gestion"
|
||||
msgstr "Assets de zones de gestion"
|
||||
|
||||
msgid "Document de zone de gestion"
|
||||
msgstr "Document de zone de gestion"
|
||||
|
||||
msgid "Documents de zones de gestion"
|
||||
msgstr "Documents de zones de gestion"
|
||||
|
||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
msgstr "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -722,3 +722,30 @@ msgstr "Erreur lors de la validation."
|
|||
msgid "Erreur réseau."
|
||||
msgstr "Erreur réseau."
|
||||
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Zones de gestion"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "Aucune zone de gestion définie pour cette thématique."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Afficher / masquer toutes les zones"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Opacité"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Fréquence cible :"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "jours"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Consignes & points d'attention"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Centrer sur la zone"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8711,3 +8711,57 @@ msgstr "Assets direct gekoppeld aan gebouw"
|
|||
|
||||
#~ msgid "Arceaux cargo constatés"
|
||||
#~ msgstr "Vastgestelde cargobeugels"
|
||||
|
||||
msgid "Type de zone de gestion"
|
||||
msgstr "Beheerszonetype"
|
||||
|
||||
msgid "Types de zones de gestion"
|
||||
msgstr "Beheerszonetypes"
|
||||
|
||||
msgid "Zone de gestion"
|
||||
msgstr "Beheerszone"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Beheerszones"
|
||||
|
||||
msgid "Consignes et points d'attention"
|
||||
msgstr "Instructies en aandachtspunten"
|
||||
|
||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
msgstr "Exploitatie-instructies, aandachtspunten, veiligheidsregels..."
|
||||
|
||||
msgid "Périmètre géographique"
|
||||
msgstr "Geografische perimeter"
|
||||
|
||||
msgid "Couleur d'affichage"
|
||||
msgstr "Weergavekleur"
|
||||
|
||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
msgstr "Hexadecimale code (bijv. #198754). Indien leeg, wordt de kleur van het zonetype gebruikt."
|
||||
|
||||
msgid "Fréquence cible (jours)"
|
||||
msgstr "Doelfrequentie (dagen)"
|
||||
|
||||
msgid "Intervalle indicatif entre interventions en jours."
|
||||
msgstr "Indicatief interval tussen interventies in dagen."
|
||||
|
||||
msgid "Saisonnalité / Calendrier"
|
||||
msgstr "Seizoensgebondenheid / Kalender"
|
||||
|
||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
msgstr "Bijv.: Maaien van april tot oktober; laat maaien in september; snoeiverbod in het voorjaar."
|
||||
|
||||
msgid "Asset de zone de gestion"
|
||||
msgstr "Beheerszone-asset"
|
||||
|
||||
msgid "Assets de zones de gestion"
|
||||
msgstr "Beheerszone-assets"
|
||||
|
||||
msgid "Document de zone de gestion"
|
||||
msgstr "Beheerszonedocument"
|
||||
|
||||
msgid "Documents de zones de gestion"
|
||||
msgstr "Beheerszonedocumenten"
|
||||
|
||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
msgstr "Indien aangevinkt, wordt dit asset uitgesloten van de zone, zelfs als de geometrie de perimeter doorsnijdt."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -719,3 +719,30 @@ msgstr "Fout bij validatie."
|
|||
msgid "Erreur réseau."
|
||||
msgstr "Netwerkfout."
|
||||
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Beheerszones"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "Geen beheerszone gedefinieerd voor dit thema."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Alle zones tonen / verbergen"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Dekking"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Doelfrequentie:"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "dagen"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Instructies & aandachtspunten"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Centreren op zone"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-05 13:06
|
||||
|
||||
import common.storages
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0029_publicbuildingdocument_floor_location_and_more'),
|
||||
('common', '0017_territory'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('documents', '0004_alter_documentfolder_visibility_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ManagementZone',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=100, unique=True, verbose_name='Code')),
|
||||
('name_fr', models.CharField(max_length=255, verbose_name='Nom (FR)')),
|
||||
('name_nl', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom (NL)')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('instructions', models.TextField(blank=True, help_text="Consignes d'exploitation, points de vigilance, règles de sécurité...", null=True, verbose_name="Consignes et points d'attention")),
|
||||
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Périmètre géographique')),
|
||||
('color', models.CharField(blank=True, help_text='Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone.', max_length=20, null=True, verbose_name="Couleur d'affichage")),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('target_frequency_days', models.IntegerField(blank=True, help_text='Intervalle indicatif entre interventions en jours.', null=True, verbose_name='Fréquence cible (jours)')),
|
||||
('seasonality_notes', models.CharField(blank=True, help_text="Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps.", max_length=255, null=True, verbose_name='Saisonnalité / Calendrier')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Date de création')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Date de mise à jour')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_management_zones', to=settings.AUTH_USER_MODEL, verbose_name='Créé par')),
|
||||
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='management_zones', to='common.thematic', verbose_name='Thématique')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Zone de gestion',
|
||||
'verbose_name_plural': 'Zones de gestion',
|
||||
'ordering': ['thematic', 'code'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagementZoneDocument',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description du document')),
|
||||
('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')),
|
||||
('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')),
|
||||
('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/management_zones/', verbose_name='Fichier')),
|
||||
('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/management_zones/')),
|
||||
('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')),
|
||||
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')),
|
||||
('zone', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.managementzone', verbose_name='Zone de gestion')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document de zone de gestion',
|
||||
'verbose_name_plural': 'Documents de zones de gestion',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagementZoneType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=100, unique=True, verbose_name='Code')),
|
||||
('name_fr', models.CharField(max_length=255, verbose_name='Nom (FR)')),
|
||||
('name_nl', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom (NL)')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('default_color', models.CharField(default='#198754', max_length=20, verbose_name='Couleur par défaut')),
|
||||
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='management_zone_types', to='common.thematic', verbose_name='Thématique')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Type de zone de gestion',
|
||||
'verbose_name_plural': 'Types de zones de gestion',
|
||||
'ordering': ['thematic', 'name_fr'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='managementzone',
|
||||
name='zone_type',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='management_zones', to='assets.managementzonetype', verbose_name='Type de zone de gestion'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagementZoneAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField(verbose_name="ID de l'asset")),
|
||||
('is_excluded', models.BooleanField(default=False, help_text='Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre.', verbose_name='Exclu')),
|
||||
('notes', models.CharField(blank=True, max_length=255, null=True, verbose_name='Notes')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset")),
|
||||
('management_zone', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='explicit_assets', to='assets.managementzone', verbose_name='Zone de gestion')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Asset de zone de gestion',
|
||||
'verbose_name_plural': 'Assets de zones de gestion',
|
||||
'unique_together': {('management_zone', 'content_type', 'object_id')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -13,4 +13,5 @@ from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitt
|
|||
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
||||
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||
from .parking import ParkingLocation, ParkingSpot
|
||||
from inspections.models import Inspection as AssetInspection, ParkingSpotInspection
|
||||
from inspections.models import Inspection as AssetInspection, ParkingSpotInspection
|
||||
from .management_zones import ManagementZone, ManagementZoneType, ManagementZoneAsset, ManagementZoneDocument
|
||||
|
|
|
|||
214
loko/assets/models/management_zones.py
Normal file
214
loko/assets/models/management_zones.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _, get_language
|
||||
import json
|
||||
|
||||
from common.models import Thematic, Document
|
||||
from common.storages import PrivateMediaStorage
|
||||
|
||||
private_storage = PrivateMediaStorage()
|
||||
|
||||
|
||||
class ManagementZoneType(models.Model):
|
||||
"""
|
||||
Typologie de zone de gestion spécifique à une thématique
|
||||
(ex: Fauche tardive / refuge biodiversité, Tonte d'agrément, Onde verte coordonnée, etc.).
|
||||
"""
|
||||
code = models.CharField(max_length=100, unique=True, verbose_name=_("Code"))
|
||||
name_fr = models.CharField(max_length=255, verbose_name=_("Nom (FR)"))
|
||||
name_nl = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Nom (NL)"))
|
||||
thematic = models.ForeignKey(
|
||||
Thematic,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="management_zone_types",
|
||||
verbose_name=_("Thématique")
|
||||
)
|
||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
||||
default_color = models.CharField(max_length=20, default="#198754", verbose_name=_("Couleur par défaut"))
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
if lang and lang.startswith("nl") and self.name_nl:
|
||||
return self.name_nl
|
||||
return self.name_fr
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_name()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_name()} ({self.thematic.name})"
|
||||
|
||||
class Meta:
|
||||
ordering = ["thematic", "name_fr"]
|
||||
verbose_name = _("Type de zone de gestion")
|
||||
verbose_name_plural = _("Types de zones de gestion")
|
||||
|
||||
|
||||
class ManagementZone(models.Model):
|
||||
"""
|
||||
Zone de gestion générique avec géométrie polygonale.
|
||||
Regroupe des équipements/assets selon une logique métier, d'exploitation ou de gestion écologique.
|
||||
"""
|
||||
code = models.CharField(max_length=100, unique=True, verbose_name=_("Code"))
|
||||
name_fr = models.CharField(max_length=255, verbose_name=_("Nom (FR)"))
|
||||
name_nl = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Nom (NL)"))
|
||||
thematic = models.ForeignKey(
|
||||
Thematic,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="management_zones",
|
||||
verbose_name=_("Thématique")
|
||||
)
|
||||
zone_type = models.ForeignKey(
|
||||
ManagementZoneType,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="management_zones",
|
||||
verbose_name=_("Type de zone de gestion")
|
||||
)
|
||||
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
|
||||
instructions = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Consignes et points d'attention"),
|
||||
help_text=_("Consignes d'exploitation, points de vigilance, règles de sécurité...")
|
||||
)
|
||||
geom = models.MultiPolygonField(
|
||||
srid=3812,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Périmètre géographique")
|
||||
)
|
||||
color = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Couleur d'affichage"),
|
||||
help_text=_("Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone.")
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
target_frequency_days = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Fréquence cible (jours)"),
|
||||
help_text=_("Intervalle indicatif entre interventions en jours.")
|
||||
)
|
||||
seasonality_notes = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Saisonnalité / Calendrier"),
|
||||
help_text=_("Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps.")
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date de création"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Date de mise à jour"))
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="created_management_zones",
|
||||
verbose_name=_("Créé par")
|
||||
)
|
||||
|
||||
def get_color(self):
|
||||
if self.color:
|
||||
return self.color
|
||||
if self.zone_type and self.zone_type.default_color:
|
||||
return self.zone_type.default_color
|
||||
return "#198754"
|
||||
|
||||
@property
|
||||
def geojson_geometry(self):
|
||||
"""
|
||||
Retourne la géométrie GeoJSON transformée en WGS84 (EPSG:4326)
|
||||
pour utilisation directe dans MapLibre / Leaflet.
|
||||
"""
|
||||
if not self.geom:
|
||||
return None
|
||||
try:
|
||||
geom_4326 = self.geom.transform(4326, clone=True)
|
||||
return json.loads(geom_4326.geojson)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_name(self):
|
||||
lang = get_language()
|
||||
if lang and lang.startswith("nl") and self.name_nl:
|
||||
return self.name_nl
|
||||
return self.name_fr
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_name()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} - {self.get_name()}"
|
||||
|
||||
class Meta:
|
||||
ordering = ["thematic", "code"]
|
||||
verbose_name = _("Zone de gestion")
|
||||
verbose_name_plural = _("Zones de gestion")
|
||||
|
||||
|
||||
class ManagementZoneAsset(models.Model):
|
||||
"""
|
||||
Liaison explicite (inclusion ou exclusion) d'un asset à une zone de gestion.
|
||||
Permet d'affiner le rattachement au-delà de la stricte inclusion spatiale.
|
||||
"""
|
||||
management_zone = models.ForeignKey(
|
||||
ManagementZone,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="explicit_assets",
|
||||
verbose_name=_("Zone de gestion")
|
||||
)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("Type d'asset"))
|
||||
object_id = models.PositiveIntegerField(verbose_name=_("ID de l'asset"))
|
||||
asset = GenericForeignKey('content_type', 'object_id')
|
||||
is_excluded = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Exclu"),
|
||||
help_text=_("Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre.")
|
||||
)
|
||||
notes = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Notes"))
|
||||
|
||||
def __str__(self):
|
||||
status = "Exclu" if self.is_excluded else "Inclus"
|
||||
return f"{self.management_zone.code} -> {self.content_type.model} #{self.object_id} ({status})"
|
||||
|
||||
class Meta:
|
||||
unique_together = ('management_zone', 'content_type', 'object_id')
|
||||
verbose_name = _("Asset de zone de gestion")
|
||||
verbose_name_plural = _("Assets de zones de gestion")
|
||||
|
||||
|
||||
class ManagementZoneDocument(Document):
|
||||
"""
|
||||
Documents, fiches techniques et arrêtés associés à une zone de gestion.
|
||||
"""
|
||||
zone = models.ForeignKey(
|
||||
ManagementZone,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="documents",
|
||||
verbose_name=_("Zone de gestion")
|
||||
)
|
||||
file = models.FileField(
|
||||
upload_to="assets_documents/management_zones/",
|
||||
storage=private_storage,
|
||||
verbose_name=_("Fichier"),
|
||||
max_length=255
|
||||
)
|
||||
thumbnail = models.ImageField(
|
||||
upload_to="assets_documents/management_zones/",
|
||||
storage=private_storage,
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=255
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Document de zone de gestion")
|
||||
verbose_name_plural = _("Documents de zones de gestion")
|
||||
390
loko/assets/static/assets/js/management_zones_map_overlay.js
Normal file
390
loko/assets/static/assets/js/management_zones_map_overlay.js
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
/**
|
||||
* Management Zones Map Overlay & Layer Switcher Integration
|
||||
*
|
||||
* Permet d'afficher en surimpression les zones de gestion (gestion écologique,
|
||||
* ondes vertes, secteurs prioritaires, etc.) sur la carte MapLibre
|
||||
* et ajoute une section dédiée "Zones de gestion" dans le LayerSwitcherControl.
|
||||
*/
|
||||
|
||||
(function (window) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Calcule la bounding box [[minX, minY], [maxX, maxY]] d'une géométrie GeoJSON.
|
||||
*/
|
||||
function getGeometryBBox(geometry) {
|
||||
if (!geometry || !geometry.coordinates) return null;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
function traverse(coords) {
|
||||
if (typeof coords[0] === 'number') {
|
||||
const [x, y] = coords;
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
} else if (Array.isArray(coords)) {
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
traverse(coords[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(geometry.coordinates);
|
||||
if (minX === Infinity || minY === Infinity) return null;
|
||||
return [[minX, minY], [maxX, maxY]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise l'overlay des zones de gestion sur la carte MapLibre.
|
||||
*/
|
||||
async function initManagementZonesMapOverlay(options = {}) {
|
||||
const map = options.map;
|
||||
if (!map) {
|
||||
console.warn('[ManagementZonesMapOverlay] Map instance missing.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!map.loaded()) {
|
||||
map.once('load', () => initManagementZonesMapOverlay(options));
|
||||
return;
|
||||
}
|
||||
|
||||
const thematic = options.thematic || 'nature';
|
||||
const geojsonUrl = options.geojsonUrl || '/assets/api/management-zones/geojson/';
|
||||
const layerSwitcher = options.layerSwitcher || window.layerSwitcher;
|
||||
const beforeLayerId = options.beforeLayerId || null;
|
||||
|
||||
const _t = (key, fallback) => (typeof gettext === 'function' ? gettext(key) : (fallback || key));
|
||||
|
||||
try {
|
||||
const url = new URL(geojsonUrl, window.location.origin);
|
||||
url.searchParams.set('thematic', thematic);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[ManagementZonesMapOverlay] Failed to fetch geojson:', response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const geojson = await response.json();
|
||||
if (!geojson || geojson.type !== 'FeatureCollection') {
|
||||
console.warn('[ManagementZonesMapOverlay] Invalid GeoJSON received.');
|
||||
return;
|
||||
}
|
||||
|
||||
const features = geojson.features || [];
|
||||
|
||||
// 1. Ajouter la source et les couches à MapLibre
|
||||
const sourceId = 'management-zones-source';
|
||||
const fillLayerId = 'management-zones-fill';
|
||||
const lineLayerId = 'management-zones-line';
|
||||
|
||||
if (map.getSource(sourceId)) {
|
||||
map.getSource(sourceId).setData(geojson);
|
||||
} else {
|
||||
map.addSource(sourceId, {
|
||||
type: 'geojson',
|
||||
data: geojson
|
||||
});
|
||||
}
|
||||
|
||||
// Couche de remplissage translucide
|
||||
if (!map.getLayer(fillLayerId)) {
|
||||
const fillLayerDef = {
|
||||
id: fillLayerId,
|
||||
type: 'fill',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'fill-color': ['coalesce', ['get', 'color'], '#198754'],
|
||||
'fill-opacity': 0.20
|
||||
}
|
||||
};
|
||||
if (beforeLayerId && map.getLayer(beforeLayerId)) {
|
||||
map.addLayer(fillLayerDef, beforeLayerId);
|
||||
} else {
|
||||
map.addLayer(fillLayerDef);
|
||||
}
|
||||
}
|
||||
|
||||
// Couche de contour avec tirets pour marquer une zone de gestion
|
||||
if (!map.getLayer(lineLayerId)) {
|
||||
const lineLayerDef = {
|
||||
id: lineLayerId,
|
||||
type: 'line',
|
||||
source: sourceId,
|
||||
paint: {
|
||||
'line-color': ['coalesce', ['get', 'color'], '#198754'],
|
||||
'line-width': 2.5,
|
||||
'line-opacity': 0.85,
|
||||
'line-dasharray': [3, 1.5]
|
||||
}
|
||||
};
|
||||
if (beforeLayerId && map.getLayer(beforeLayerId)) {
|
||||
map.addLayer(lineLayerDef, beforeLayerId);
|
||||
} else {
|
||||
map.addLayer(lineLayerDef);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Gestion des popups et du curseur sur clic
|
||||
const zonePopup = new maplibregl.Popup({
|
||||
closeButton: true,
|
||||
closeOnClick: true,
|
||||
maxWidth: '340px'
|
||||
});
|
||||
|
||||
map.on('click', fillLayerId, (e) => {
|
||||
const assetLayerIds = (window.assets_layers_defs || []).map(l => l.id).filter(id => map.getLayer(id));
|
||||
if (assetLayerIds.length > 0) {
|
||||
const rendered = map.queryRenderedFeatures(e.point, { layers: assetLayerIds });
|
||||
if (rendered && rendered.length > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!e.features || !e.features[0]) return;
|
||||
const props = e.features[0].properties || {};
|
||||
|
||||
let typeHtml = '';
|
||||
if (props.zone_type_name) {
|
||||
typeHtml = `<span class="badge me-1" style="background-color: ${props.color || '#198754'}; color: #fff; font-size: 10px;">
|
||||
${props.zone_type_name}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
let freqHtml = '';
|
||||
if (props.target_frequency_days) {
|
||||
freqHtml = `<div style="color: #64748b; font-size: 11px; margin-top: 3px;">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>${_t('Fréquence cible :', 'Fréquence cible :')} <strong>${props.target_frequency_days} ${_t('jours', 'jours')}</strong>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let seasonHtml = '';
|
||||
if (props.seasonality_notes) {
|
||||
seasonHtml = `<div style="color: #64748b; font-size: 11px; margin-top: 3px;">
|
||||
<i class="bi bi-calendar3 me-1"></i>${props.seasonality_notes}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let instructionsHtml = '';
|
||||
if (props.instructions) {
|
||||
instructionsHtml = `<div style="background-color: #f8fafc; border-left: 3px solid ${props.color || '#198754'}; padding: 6px; margin-top: 6px; font-size: 11px; color: #334155; border-radius: 2px; max-height: 100px; overflow-y: auto;">
|
||||
<div style="font-weight: 600; margin-bottom: 2px;"><i class="bi bi-exclamation-triangle me-1"></i>${_t('Consignes & points d\'attention', 'Consignes & points d\'attention')}</div>
|
||||
${props.instructions}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let descHtml = '';
|
||||
if (props.description) {
|
||||
descHtml = `<div style="color: #475569; font-size: 11px; margin-top: 4px;">
|
||||
${props.description}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const html = `
|
||||
<div style="font-family: inherit; font-size: 12px; padding: 2px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 4px;">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 3px; background-color: ${props.color || '#198754'}; border: 1px solid rgba(0,0,0,0.2); flex-shrink: 0;"></span>
|
||||
<strong style="font-size: 13px; color: #1e293b;">${props.code}</strong>
|
||||
</div>
|
||||
${typeHtml}
|
||||
</div>
|
||||
<div style="font-weight: 600; color: #1e293b; margin-bottom: 4px;">${props.name}</div>
|
||||
${descHtml}
|
||||
${freqHtml}
|
||||
${seasonHtml}
|
||||
${instructionsHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
zonePopup.setLngLat(e.lngLat).setHTML(html).addTo(map);
|
||||
});
|
||||
|
||||
map.on('mousemove', fillLayerId, (e) => {
|
||||
const assetLayerIds = (window.assets_layers_defs || []).map(l => l.id).filter(id => map.getLayer(id));
|
||||
if (assetLayerIds.length > 0) {
|
||||
const rendered = map.queryRenderedFeatures(e.point, { layers: assetLayerIds });
|
||||
if (rendered && rendered.length > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
map.getCanvas().style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
map.on('mouseleave', fillLayerId, () => {
|
||||
map.getCanvas().style.cursor = '';
|
||||
});
|
||||
|
||||
// 3. Intégration dans le LayerSwitcherControl
|
||||
if (layerSwitcher && typeof layerSwitcher.addCustomSection === 'function') {
|
||||
layerSwitcher.addCustomSection({
|
||||
id: 'management-zones',
|
||||
title: `<span class="d-flex align-items-center gap-1"><i class="bi bi-bounding-box-circles me-1"></i>${_t('Zones de gestion', 'Zones de gestion')}</span>`,
|
||||
render: (container, switcher) => {
|
||||
renderManagementZonesSection(container, switcher, features, map, fillLayerId, lineLayerId, zonePopup, _t);
|
||||
}
|
||||
});
|
||||
|
||||
if (features.length > 0) {
|
||||
layerSwitcher.setOverlayActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('[ManagementZonesMapOverlay] Error initializing overlay:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendu HTML et interactions de la section "Zones de gestion" dans le LayerSwitcherControl.
|
||||
*/
|
||||
function renderManagementZonesSection(container, switcher, features, map, fillLayerId, lineLayerId, zonePopup, _t) {
|
||||
if (!features || features.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-muted small px-2 py-1 fst-italic">
|
||||
${_t('Aucune zone de gestion définie pour cette thématique.', 'Aucune zone de gestion définie pour cette thématique.')}
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const countText = `${features.length} ${_t('zones', 'zones')}`;
|
||||
|
||||
// En-tête avec toggle général
|
||||
const headerEl = document.createElement('div');
|
||||
headerEl.className = 'd-flex justify-content-between align-items-center px-1 mb-2';
|
||||
headerEl.innerHTML = `
|
||||
<span class="text-muted small fw-semibold">${countText}</span>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input management-zones-master-toggle" type="checkbox" role="switch" checked title="${_t('Afficher / masquer toutes les zones', 'Afficher / masquer toutes les zones')}">
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(headerEl);
|
||||
|
||||
const masterToggle = headerEl.querySelector('.management-zones-master-toggle');
|
||||
|
||||
// Curseur d'opacité
|
||||
const opacityRow = document.createElement('div');
|
||||
opacityRow.className = 'px-1 mb-2';
|
||||
opacityRow.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center small text-muted mb-1">
|
||||
<span>${_t('Opacité', 'Opacité')}</span>
|
||||
<span class="management-zones-opacity-val">20%</span>
|
||||
</div>
|
||||
<input type="range" class="form-range management-zones-opacity-slider" min="5" max="80" value="20" style="height: 4px;">
|
||||
`;
|
||||
container.appendChild(opacityRow);
|
||||
|
||||
const opacitySlider = opacityRow.querySelector('.management-zones-opacity-slider');
|
||||
const opacityValLabel = opacityRow.querySelector('.management-zones-opacity-val');
|
||||
|
||||
opacitySlider.addEventListener('input', (e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
opacityValLabel.textContent = `${val}%`;
|
||||
if (map.getLayer(fillLayerId)) {
|
||||
map.setPaintProperty(fillLayerId, 'fill-opacity', val / 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Liste des zones
|
||||
const listEl = document.createElement('div');
|
||||
listEl.className = 'management-zones-list d-flex flex-column gap-1';
|
||||
listEl.style.maxHeight = '240px';
|
||||
listEl.style.overflowY = 'auto';
|
||||
|
||||
const activeZoneIds = new Set(features.map(f => f.id));
|
||||
|
||||
function updateLayersVisibility() {
|
||||
if (!masterToggle.checked || activeZoneIds.size === 0) {
|
||||
if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, 'visibility', 'none');
|
||||
if (map.getLayer(lineLayerId)) map.setLayoutProperty(lineLayerId, 'visibility', 'none');
|
||||
} else {
|
||||
if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, 'visibility', 'visible');
|
||||
if (map.getLayer(lineLayerId)) map.setLayoutProperty(lineLayerId, 'visibility', 'visible');
|
||||
|
||||
const filter = ['in', ['id'], ['literal', Array.from(activeZoneIds)]];
|
||||
if (map.getLayer(fillLayerId)) map.setFilter(fillLayerId, filter);
|
||||
if (map.getLayer(lineLayerId)) map.setFilter(lineLayerId, filter);
|
||||
}
|
||||
}
|
||||
|
||||
masterToggle.addEventListener('change', () => {
|
||||
const checked = masterToggle.checked;
|
||||
listEl.querySelectorAll('.zone-item-toggle').forEach(chk => {
|
||||
chk.checked = checked;
|
||||
});
|
||||
if (checked) {
|
||||
features.forEach(f => activeZoneIds.add(f.id));
|
||||
} else {
|
||||
activeZoneIds.clear();
|
||||
}
|
||||
updateLayersVisibility();
|
||||
});
|
||||
|
||||
features.forEach(feat => {
|
||||
const props = feat.properties || {};
|
||||
const color = props.color || '#198754';
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.className = 'd-flex align-items-center justify-content-between p-1 rounded hover-bg small';
|
||||
itemEl.style.cursor = 'pointer';
|
||||
|
||||
itemEl.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-2 text-truncate me-2 zone-click-target" style="flex: 1;">
|
||||
<span style="display:inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${color}; border: 1px solid rgba(0,0,0,0.25); flex-shrink: 0;"></span>
|
||||
<div class="text-truncate">
|
||||
<div class="fw-semibold text-truncate text-dark" style="font-size: 11px;">${props.code}</div>
|
||||
<div class="text-muted text-truncate" style="font-size: 10px;">${props.name} ${props.zone_type_name ? `(${props.zone_type_name})` : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1 flex-shrink-0">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted zoom-btn" title="${_t('Centrer sur la zone', 'Centrer sur la zone')}">
|
||||
<i class="bi bi-geo-alt" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0 ms-1">
|
||||
<input class="form-check-input zone-item-toggle" type="checkbox" role="switch" checked data-zone-id="${feat.id}">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const itemToggle = itemEl.querySelector('.zone-item-toggle');
|
||||
itemToggle.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
if (itemToggle.checked) {
|
||||
activeZoneIds.add(feat.id);
|
||||
} else {
|
||||
activeZoneIds.delete(feat.id);
|
||||
}
|
||||
masterToggle.checked = (activeZoneIds.size > 0);
|
||||
updateLayersVisibility();
|
||||
});
|
||||
|
||||
function zoomToZone() {
|
||||
const bbox = getGeometryBBox(feat.geometry);
|
||||
if (bbox) {
|
||||
map.fitBounds(bbox, { padding: 40, maxZoom: 16 });
|
||||
}
|
||||
}
|
||||
|
||||
itemEl.querySelector('.zoom-btn').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
zoomToZone();
|
||||
});
|
||||
|
||||
itemEl.querySelector('.zone-click-target').addEventListener('click', () => {
|
||||
zoomToZone();
|
||||
});
|
||||
|
||||
listEl.appendChild(itemEl);
|
||||
});
|
||||
|
||||
container.appendChild(listEl);
|
||||
}
|
||||
|
||||
window.initManagementZonesMapOverlay = initManagementZonesMapOverlay;
|
||||
|
||||
})(window);
|
||||
|
|
@ -366,6 +366,7 @@
|
|||
<script src="{% static 'common/base_map.js' %}"></script>
|
||||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
||||
<script src="{% static 'contracts/js/contracts_map_overlay.js' %}"></script>
|
||||
<script src="{% static 'assets/js/management_zones_map_overlay.js' %}"></script>
|
||||
<script src="{% static 'assets/assets_index.js' %}"></script>
|
||||
{% block extra_script %}
|
||||
{% endblock extra_script %}
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@
|
|||
{% block extra_script %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function setupNatureContractsOverlay() {
|
||||
function setupNatureOverlays() {
|
||||
if (typeof initContractsMapOverlay === 'function' && window.map) {
|
||||
initContractsMapOverlay({
|
||||
map: window.map,
|
||||
|
|
@ -232,16 +232,24 @@
|
|||
geojsonUrl: "{% url 'contracts:contracts_geojson' %}",
|
||||
});
|
||||
}
|
||||
if (typeof initManagementZonesMapOverlay === 'function' && window.map) {
|
||||
initManagementZonesMapOverlay({
|
||||
map: window.map,
|
||||
layerSwitcher: window.layerSwitcher,
|
||||
thematic: 'nature',
|
||||
geojsonUrl: "{% url 'assets:management_zones_geojson' %}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (window.map && window.map.loaded && window.map.loaded()) {
|
||||
setupNatureContractsOverlay();
|
||||
setupNatureOverlays();
|
||||
} else if (window.maploader) {
|
||||
window.maploader.addEventListener('mapLoaded', () => {
|
||||
setTimeout(setupNatureContractsOverlay, 100);
|
||||
setTimeout(setupNatureOverlays, 100);
|
||||
});
|
||||
} else if (window.map) {
|
||||
window.map.on('load', setupNatureContractsOverlay);
|
||||
window.map.on('load', setupNatureOverlays);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
201
loko/assets/tests_management_zones.py
Normal file
201
loko/assets/tests_management_zones.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
from django.test import TestCase, Client
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.contrib.gis.geos import Polygon, MultiPolygon, Point
|
||||
|
||||
from common.models import Thematic, Role, UserConfig, UserThematics
|
||||
from assets.models import (
|
||||
ManagementZoneType,
|
||||
ManagementZone,
|
||||
ManagementZoneAsset,
|
||||
NatureGreenSurface,
|
||||
NatureLocation,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ManagementZoneModelTests(TestCase):
|
||||
def setUp(self):
|
||||
self.thematic_nature = Thematic.objects.create(
|
||||
code="nature",
|
||||
name_fr="Nature",
|
||||
name_nl="Natuur"
|
||||
)
|
||||
self.zone_type = ManagementZoneType.objects.create(
|
||||
code="fauche_tardive",
|
||||
name_fr="Fauche tardive",
|
||||
name_nl="Laat maaien",
|
||||
thematic=self.thematic_nature,
|
||||
default_color="#28a745",
|
||||
description="Zone de gestion écologique avec fauche annuelle en automne."
|
||||
)
|
||||
|
||||
def test_management_zone_type_bilingual_and_str(self):
|
||||
self.assertEqual(self.zone_type.name_fr, "Fauche tardive")
|
||||
self.assertEqual(self.zone_type.name_nl, "Laat maaien")
|
||||
self.assertIn("Fauche tardive", str(self.zone_type))
|
||||
|
||||
def test_management_zone_creation_geometry_and_color_fallback(self):
|
||||
poly = Polygon(((0, 0), (100, 0), (100, 100), (0, 100), (0, 0)))
|
||||
multi_poly = MultiPolygon([poly], srid=3812)
|
||||
|
||||
# 1. Sans couleur explicite -> prend la couleur par défaut du type
|
||||
zone1 = ManagementZone.objects.create(
|
||||
code="ZG-NAT-001",
|
||||
name_fr="Marais de Jette - Prairie",
|
||||
name_nl="Moeras van Jette - Weide",
|
||||
thematic=self.thematic_nature,
|
||||
zone_type=self.zone_type,
|
||||
geom=multi_poly,
|
||||
target_frequency_days=365,
|
||||
seasonality_notes="Fauche en septembre; pas de taille au printemps",
|
||||
instructions="Attention aux zones humides et aux ornières.",
|
||||
)
|
||||
self.assertEqual(zone1.get_color(), "#28a745")
|
||||
self.assertEqual(zone1.geom.srid, 3812)
|
||||
self.assertEqual(zone1.geom.geom_type, "MultiPolygon")
|
||||
|
||||
# GeoJSON property
|
||||
geojson = zone1.geojson_geometry
|
||||
self.assertIsNotNone(geojson)
|
||||
self.assertEqual(geojson["type"], "MultiPolygon")
|
||||
|
||||
# Spatial intersection
|
||||
pt_inside = Point(50, 50, srid=3812)
|
||||
pt_outside = Point(200, 200, srid=3812)
|
||||
self.assertTrue(ManagementZone.objects.filter(pk=zone1.pk, geom__intersects=pt_inside).exists())
|
||||
self.assertFalse(ManagementZone.objects.filter(pk=zone1.pk, geom__intersects=pt_outside).exists())
|
||||
|
||||
# 2. Avec couleur personnalisée explicite
|
||||
zone2 = ManagementZone.objects.create(
|
||||
code="ZG-NAT-002",
|
||||
name_fr="Zone spéciale",
|
||||
thematic=self.thematic_nature,
|
||||
color="#ff5722",
|
||||
)
|
||||
self.assertEqual(zone2.get_color(), "#ff5722")
|
||||
|
||||
|
||||
class ManagementZoneAssetTests(TestCase):
|
||||
def setUp(self):
|
||||
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
|
||||
self.zone = ManagementZone.objects.create(
|
||||
code="ZG-NAT-TEST",
|
||||
name_fr="Zone test",
|
||||
thematic=self.thematic_nature,
|
||||
)
|
||||
self.location = NatureLocation.objects.create(code="LOC-01", name_fr="Parc de Jette")
|
||||
self.green_surface = NatureGreenSurface.objects.create(
|
||||
code="GS-01",
|
||||
name_fr="Pelouse 1",
|
||||
location=self.location,
|
||||
)
|
||||
|
||||
def test_explicit_asset_association_and_exclusion(self):
|
||||
# Association explicite d'inclusion
|
||||
link = ManagementZoneAsset.objects.create(
|
||||
management_zone=self.zone,
|
||||
asset=self.green_surface,
|
||||
is_excluded=False,
|
||||
notes="Inclus dans le plan"
|
||||
)
|
||||
self.assertIn("Inclus", str(link))
|
||||
self.assertEqual(self.zone.explicit_assets.count(), 1)
|
||||
self.assertEqual(self.zone.explicit_assets.first().asset, self.green_surface)
|
||||
|
||||
# Bascule en exclusion
|
||||
link.is_excluded = True
|
||||
link.save()
|
||||
self.assertIn("Exclu", str(link))
|
||||
|
||||
|
||||
class ManagementZoneGeojsonEndpointTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="nature_manager", password="password")
|
||||
self.role_manager = Role.objects.create(name="manager")
|
||||
self.config = UserConfig.objects.create(user=self.user)
|
||||
self.config.roles.add(self.role_manager)
|
||||
|
||||
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
|
||||
self.thematic_roads = Thematic.objects.create(code="roads", name_fr="Voiries", name_nl="Wegen")
|
||||
|
||||
UserThematics.objects.create(
|
||||
user_config=self.config,
|
||||
thematic=self.thematic_nature,
|
||||
can_view_assets=True,
|
||||
can_edit_assets=True
|
||||
)
|
||||
|
||||
self.zone_type = ManagementZoneType.objects.create(
|
||||
code="fauche",
|
||||
name_fr="Fauche tardive",
|
||||
thematic=self.thematic_nature,
|
||||
default_color="#198754"
|
||||
)
|
||||
|
||||
poly = Polygon(((0, 0), (10, 0), (10, 10), (0, 10), (0, 0)))
|
||||
self.multi_poly = MultiPolygon([poly], srid=3812)
|
||||
|
||||
self.zone_nature = ManagementZone.objects.create(
|
||||
code="ZG-NAT-1",
|
||||
name_fr="Prairie fleurie",
|
||||
thematic=self.thematic_nature,
|
||||
zone_type=self.zone_type,
|
||||
geom=self.multi_poly,
|
||||
instructions="Ne pas tondre au printemps",
|
||||
target_frequency_days=180,
|
||||
seasonality_notes="Septembre",
|
||||
)
|
||||
|
||||
self.zone_roads = ManagementZone.objects.create(
|
||||
code="ZG-ROAD-1",
|
||||
name_fr="Itinéraire déneigement P1",
|
||||
thematic=self.thematic_roads,
|
||||
geom=self.multi_poly,
|
||||
color="#0d6efd",
|
||||
)
|
||||
|
||||
def test_anonymous_access_redirects(self):
|
||||
url = reverse("assets:management_zones_geojson")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_authenticated_user_access_nature_zones(self):
|
||||
self.client.login(username="nature_manager", password="password")
|
||||
url = reverse("assets:management_zones_geojson") + "?thematic=nature"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.json()
|
||||
self.assertEqual(data["type"], "FeatureCollection")
|
||||
self.assertEqual(len(data["features"]), 1)
|
||||
|
||||
feat = data["features"][0]
|
||||
self.assertEqual(feat["id"], self.zone_nature.id)
|
||||
self.assertEqual(feat["properties"]["code"], "ZG-NAT-1")
|
||||
self.assertEqual(feat["properties"]["name"], "Prairie fleurie")
|
||||
self.assertEqual(feat["properties"]["thematic"], "nature")
|
||||
self.assertEqual(feat["properties"]["zone_type_name"], "Fauche tardive")
|
||||
self.assertEqual(feat["properties"]["color"], "#198754")
|
||||
self.assertEqual(feat["properties"]["instructions"], "Ne pas tondre au printemps")
|
||||
self.assertEqual(feat["properties"]["target_frequency_days"], 180)
|
||||
self.assertEqual(feat["geometry"]["type"], "MultiPolygon")
|
||||
|
||||
def test_user_without_thematic_permission_denied(self):
|
||||
self.client.login(username="nature_manager", password="password")
|
||||
# nature_manager n'a pas accès à la thématique 'roads'
|
||||
url = reverse("assets:management_zones_geojson") + "?thematic=roads"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_superuser_access_all_thematics(self):
|
||||
admin = User.objects.create_superuser(username="superadmin", password="password", email="admin@example.com")
|
||||
self.client.login(username="superadmin", password="password")
|
||||
|
||||
# Sans paramètre thématique, superuser reçoit toutes les zones avec géométrie
|
||||
url = reverse("assets:management_zones_geojson")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(len(data["features"]), 2)
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
from django.urls import path
|
||||
from .views import core, locations, nature, publiclighting, trafficlights, artworks, structures, controlcenters, sign, roads, its, clean, asset_editing, history, location_editing, export, asset_permissions, water, buildings, outdoor, parking
|
||||
from .views import core, locations, nature, publiclighting, trafficlights, artworks, structures, controlcenters, sign, roads, its, clean, asset_editing, history, location_editing, export, asset_permissions, water, buildings, outdoor, parking, management_zones
|
||||
|
||||
app_name = "assets"
|
||||
urlpatterns = [
|
||||
path("", core.unified_assets_index, name="index"),
|
||||
path("api/management-zones/geojson/", management_zones.management_zones_geojson, name="management_zones_geojson"),
|
||||
path("parking/", parking.parking_index, name="parking_index"),
|
||||
path("parking/assets/", parking.parking_assets, name="parking_assets"),
|
||||
path("parking/assets/geojson/", parking.parking_assets_geojson, name="parking_assets_geojson"),
|
||||
|
|
|
|||
87
loko/assets/views/management_zones.py
Normal file
87
loko/assets/views/management_zones.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import JsonResponse, HttpResponseForbidden
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
from common.models import Thematic, UserConfig
|
||||
from assets.models import ManagementZone
|
||||
from assets.permissions import get_user_asset_thematic_access
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def management_zones_geojson(request):
|
||||
"""
|
||||
Retourne la collection GeoJSON des zones de gestion actives avec géométrie.
|
||||
Filtre optionnel par ?thematic=<thematic_code>.
|
||||
Vérifie que l'utilisateur a accès en lecture aux assets de la thématique.
|
||||
"""
|
||||
thematic_code = request.GET.get('thematic')
|
||||
user = request.user
|
||||
|
||||
if thematic_code:
|
||||
thematic = Thematic.objects.filter(code=thematic_code).first()
|
||||
if not thematic:
|
||||
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
||||
|
||||
can_view, _ = get_user_asset_thematic_access(user, thematic)
|
||||
if not can_view:
|
||||
return HttpResponseForbidden("Access denied to this thematic management zones.")
|
||||
|
||||
zones_qs = ManagementZone.objects.filter(
|
||||
thematic=thematic,
|
||||
is_active=True,
|
||||
geom__isnull=False
|
||||
).select_related('thematic', 'zone_type')
|
||||
else:
|
||||
if user.is_superuser:
|
||||
zones_qs = ManagementZone.objects.filter(is_active=True, geom__isnull=False)
|
||||
else:
|
||||
try:
|
||||
user_config = UserConfig.objects.get(user=user)
|
||||
if user_config.roles.filter(name__in=['admin', 'operator']).exists():
|
||||
allowed_thematics = Thematic.objects.all()
|
||||
else:
|
||||
allowed_thematics = Thematic.objects.filter(
|
||||
userthematics__user_config=user_config,
|
||||
userthematics__can_view_assets=True
|
||||
)
|
||||
zones_qs = ManagementZone.objects.filter(
|
||||
thematic__in=allowed_thematics,
|
||||
is_active=True,
|
||||
geom__isnull=False
|
||||
)
|
||||
except UserConfig.DoesNotExist:
|
||||
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
||||
|
||||
zones_qs = zones_qs.select_related('thematic', 'zone_type')
|
||||
|
||||
features = []
|
||||
for zone in zones_qs:
|
||||
geom_json = zone.geojson_geometry
|
||||
if not geom_json:
|
||||
continue
|
||||
|
||||
features.append({
|
||||
'type': 'Feature',
|
||||
'id': zone.id,
|
||||
'properties': {
|
||||
'id': zone.id,
|
||||
'code': zone.code,
|
||||
'name': zone.get_name(),
|
||||
'thematic': zone.thematic.code,
|
||||
'thematic_name': zone.thematic.name,
|
||||
'zone_type_code': zone.zone_type.code if zone.zone_type else None,
|
||||
'zone_type_name': zone.zone_type.get_name() if zone.zone_type else "",
|
||||
'color': zone.get_color(),
|
||||
'instructions': zone.instructions or "",
|
||||
'description': zone.description or "",
|
||||
'seasonality_notes': zone.seasonality_notes or "",
|
||||
'target_frequency_days': zone.target_frequency_days,
|
||||
},
|
||||
'geometry': geom_json
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'type': 'FeatureCollection',
|
||||
'features': features
|
||||
})
|
||||
Binary file not shown.
|
|
@ -1303,3 +1303,57 @@ msgstr "lots"
|
|||
msgid "Fonds de plan & Orthophotos"
|
||||
msgstr "Basemaps & Orthophotos"
|
||||
|
||||
|
||||
msgid "Type de zone de gestion"
|
||||
msgstr "Management zone type"
|
||||
|
||||
msgid "Types de zones de gestion"
|
||||
msgstr "Management zone types"
|
||||
|
||||
msgid "Zone de gestion"
|
||||
msgstr "Management zone"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Management zones"
|
||||
|
||||
msgid "Consignes et points d'attention"
|
||||
msgstr "Instructions and points of attention"
|
||||
|
||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
msgstr "Operational instructions, points of vigilance, safety rules..."
|
||||
|
||||
msgid "Périmètre géographique"
|
||||
msgstr "Geographical perimeter"
|
||||
|
||||
msgid "Couleur d'affichage"
|
||||
msgstr "Display color"
|
||||
|
||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
msgstr "Hexadecimal code (e.g. #198754). If empty, the zone type color is used."
|
||||
|
||||
msgid "Fréquence cible (jours)"
|
||||
msgstr "Target frequency (days)"
|
||||
|
||||
msgid "Intervalle indicatif entre interventions en jours."
|
||||
msgstr "Indicative interval between interventions in days."
|
||||
|
||||
msgid "Saisonnalité / Calendrier"
|
||||
msgstr "Seasonality / Calendar"
|
||||
|
||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
msgstr "E.g.: Mowing from April to October; late mowing in September; pruning prohibited in spring."
|
||||
|
||||
msgid "Asset de zone de gestion"
|
||||
msgstr "Management zone asset"
|
||||
|
||||
msgid "Assets de zones de gestion"
|
||||
msgstr "Management zone assets"
|
||||
|
||||
msgid "Document de zone de gestion"
|
||||
msgstr "Management zone document"
|
||||
|
||||
msgid "Documents de zones de gestion"
|
||||
msgstr "Management zone documents"
|
||||
|
||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
msgstr "If checked, this asset is excluded from the zone even if its geometry intersects the perimeter."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -194,3 +194,30 @@ msgstr "lots"
|
|||
|
||||
msgid "Fonds de plan & Orthophotos"
|
||||
msgstr "Basemaps & Orthophotos"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Management zones"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "No management zone defined for this thematic."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Show / hide all zones"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Opacity"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Target frequency:"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "days"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Instructions & points of attention"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Center on zone"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1335,3 +1335,57 @@ msgstr "lots"
|
|||
msgid "Fonds de plan & Orthophotos"
|
||||
msgstr "Fonds de plan & Orthophotos"
|
||||
|
||||
|
||||
msgid "Type de zone de gestion"
|
||||
msgstr "Type de zone de gestion"
|
||||
|
||||
msgid "Types de zones de gestion"
|
||||
msgstr "Types de zones de gestion"
|
||||
|
||||
msgid "Zone de gestion"
|
||||
msgstr "Zone de gestion"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Zones de gestion"
|
||||
|
||||
msgid "Consignes et points d'attention"
|
||||
msgstr "Consignes et points d'attention"
|
||||
|
||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
msgstr "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
|
||||
msgid "Périmètre géographique"
|
||||
msgstr "Périmètre géographique"
|
||||
|
||||
msgid "Couleur d'affichage"
|
||||
msgstr "Couleur d'affichage"
|
||||
|
||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
msgstr "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
|
||||
msgid "Fréquence cible (jours)"
|
||||
msgstr "Fréquence cible (jours)"
|
||||
|
||||
msgid "Intervalle indicatif entre interventions en jours."
|
||||
msgstr "Intervalle indicatif entre interventions en jours."
|
||||
|
||||
msgid "Saisonnalité / Calendrier"
|
||||
msgstr "Saisonnalité / Calendrier"
|
||||
|
||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
msgstr "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
|
||||
msgid "Asset de zone de gestion"
|
||||
msgstr "Asset de zone de gestion"
|
||||
|
||||
msgid "Assets de zones de gestion"
|
||||
msgstr "Assets de zones de gestion"
|
||||
|
||||
msgid "Document de zone de gestion"
|
||||
msgstr "Document de zone de gestion"
|
||||
|
||||
msgid "Documents de zones de gestion"
|
||||
msgstr "Documents de zones de gestion"
|
||||
|
||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
msgstr "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -194,3 +194,30 @@ msgstr "lots"
|
|||
|
||||
msgid "Fonds de plan & Orthophotos"
|
||||
msgstr "Fonds de plan & Orthophotos"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Zones de gestion"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "Aucune zone de gestion définie pour cette thématique."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Afficher / masquer toutes les zones"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Opacité"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Fréquence cible :"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "jours"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Consignes & points d'attention"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Centrer sur la zone"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1305,3 +1305,57 @@ msgstr "percelen"
|
|||
msgid "Fonds de plan & Orthophotos"
|
||||
msgstr "Achtergrondkaarten & Orthofoto's"
|
||||
|
||||
|
||||
msgid "Type de zone de gestion"
|
||||
msgstr "Beheerszonetype"
|
||||
|
||||
msgid "Types de zones de gestion"
|
||||
msgstr "Beheerszonetypes"
|
||||
|
||||
msgid "Zone de gestion"
|
||||
msgstr "Beheerszone"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Beheerszones"
|
||||
|
||||
msgid "Consignes et points d'attention"
|
||||
msgstr "Instructies en aandachtspunten"
|
||||
|
||||
msgid "Consignes d'exploitation, points de vigilance, règles de sécurité..."
|
||||
msgstr "Exploitatie-instructies, aandachtspunten, veiligheidsregels..."
|
||||
|
||||
msgid "Périmètre géographique"
|
||||
msgstr "Geografische perimeter"
|
||||
|
||||
msgid "Couleur d'affichage"
|
||||
msgstr "Weergavekleur"
|
||||
|
||||
msgid "Code hexadécimal (ex: #198754). Si vide, utilise la couleur du type de zone."
|
||||
msgstr "Hexadecimale code (bijv. #198754). Indien leeg, wordt de kleur van het zonetype gebruikt."
|
||||
|
||||
msgid "Fréquence cible (jours)"
|
||||
msgstr "Doelfrequentie (dagen)"
|
||||
|
||||
msgid "Intervalle indicatif entre interventions en jours."
|
||||
msgstr "Indicatief interval tussen interventies in dagen."
|
||||
|
||||
msgid "Saisonnalité / Calendrier"
|
||||
msgstr "Seizoensgebondenheid / Kalender"
|
||||
|
||||
msgid "Ex: Tonte d'avril à octobre; fauche tardive en septembre; interdiction de taille au printemps."
|
||||
msgstr "Bijv.: Maaien van april tot oktober; laat maaien in september; snoeiverbod in het voorjaar."
|
||||
|
||||
msgid "Asset de zone de gestion"
|
||||
msgstr "Beheerszone-asset"
|
||||
|
||||
msgid "Assets de zones de gestion"
|
||||
msgstr "Beheerszone-assets"
|
||||
|
||||
msgid "Document de zone de gestion"
|
||||
msgstr "Beheerszonedocument"
|
||||
|
||||
msgid "Documents de zones de gestion"
|
||||
msgstr "Beheerszonedocumenten"
|
||||
|
||||
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
|
||||
msgstr "Indien aangevinkt, wordt dit asset uitgesloten van de zone, zelfs als de geometrie de perimeter doorsnijdt."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -194,3 +194,30 @@ msgstr "percelen"
|
|||
|
||||
msgid "Fonds de plan & Orthophotos"
|
||||
msgstr "Achtergrondkaarten & Orthofoto's"
|
||||
|
||||
msgid "Zones de gestion"
|
||||
msgstr "Beheerszones"
|
||||
|
||||
msgid "Aucune zone de gestion définie pour cette thématique."
|
||||
msgstr "Geen beheerszone gedefinieerd voor dit thema."
|
||||
|
||||
msgid "zones"
|
||||
msgstr "zones"
|
||||
|
||||
msgid "Afficher / masquer toutes les zones"
|
||||
msgstr "Alle zones tonen / verbergen"
|
||||
|
||||
msgid "Opacité"
|
||||
msgstr "Dekking"
|
||||
|
||||
msgid "Fréquence cible :"
|
||||
msgstr "Doelfrequentie:"
|
||||
|
||||
msgid "jours"
|
||||
msgstr "dagen"
|
||||
|
||||
msgid "Consignes & points d'attention"
|
||||
msgstr "Instructies & aandachtspunten"
|
||||
|
||||
msgid "Centrer sur la zone"
|
||||
msgstr "Centreren op zone"
|
||||
|
|
|
|||
Loading…
Reference in a new issue