feat(interventions): afficher et sélectionner les bornes ITS et caméras sur la carte
- Ajout de l'endpoint GeoJSON pour les traverses, plots de bornes et caméras ITS - Affichage des lignes, points et étiquettes de code avec halo sur la carte d'ajout - Activation/désactivation dynamique selon la catégorie de bornes rétractables - Sélection directe de l'asset au clic sur la carte avec conservation des coordonnées cliquées - Ajout des tests unitaires automatisés pour l'endpoint GeoJSON
This commit is contained in:
parent
611df933e5
commit
ee73aa3912
6 changed files with 926 additions and 15 deletions
311
loko/assets/tests_its_bollard_systems.py
Normal file
311
loko/assets/tests_its_bollard_systems.py
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
import json
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.gis.geos import Point, LineString
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from common.models import Thematic, Role, UserConfig, UserThematics, UserContractAccess
|
||||||
|
from contracts.models import Contract
|
||||||
|
from assets.models.its import (
|
||||||
|
ITSLocation, ITSBollardSystem, ITSBollardPlot,
|
||||||
|
ITSBollardSystemAsset, ITSBollardSystemContract, ITSGeoAsset
|
||||||
|
)
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class ITSBollardSystemsGeojsonTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.thematic_its, _ = Thematic.objects.get_or_create(
|
||||||
|
code="its",
|
||||||
|
defaults={"name_fr": "Systèmes de transport intelligents", "name_nl": "ITS"}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.role_admin, _ = Role.objects.get_or_create(name="admin")
|
||||||
|
|
||||||
|
# Superuser
|
||||||
|
self.superuser = User.objects.create_superuser(
|
||||||
|
username="admin_its",
|
||||||
|
email="admin_its@example.com",
|
||||||
|
password="password123"
|
||||||
|
)
|
||||||
|
UserConfig.objects.get_or_create(user=self.superuser, defaults={"is_intern": True})
|
||||||
|
|
||||||
|
# Internal user with ITS view access
|
||||||
|
self.user_internal = User.objects.create_user(
|
||||||
|
username="user_internal",
|
||||||
|
email="user_internal@example.com",
|
||||||
|
password="password123"
|
||||||
|
)
|
||||||
|
cfg_internal, _ = UserConfig.objects.get_or_create(
|
||||||
|
user=self.user_internal,
|
||||||
|
defaults={"is_intern": True, "limit_assets_to_contracts": False}
|
||||||
|
)
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=cfg_internal,
|
||||||
|
thematic=self.thematic_its,
|
||||||
|
can_view_assets=True,
|
||||||
|
can_edit_assets=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# External user restricted to contracts
|
||||||
|
self.user_external = User.objects.create_user(
|
||||||
|
username="user_external",
|
||||||
|
email="user_external@example.com",
|
||||||
|
password="password123"
|
||||||
|
)
|
||||||
|
self.cfg_external, _ = UserConfig.objects.get_or_create(
|
||||||
|
user=self.user_external,
|
||||||
|
defaults={"is_intern": False, "limit_assets_to_contracts": True}
|
||||||
|
)
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=self.cfg_external,
|
||||||
|
thematic=self.thematic_its,
|
||||||
|
can_view_assets=True,
|
||||||
|
can_edit_assets=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Company & Contracts
|
||||||
|
from datetime import date
|
||||||
|
from contracts.models import Company
|
||||||
|
|
||||||
|
self.company = Company.objects.create(name="ITS Maintenance Corp")
|
||||||
|
|
||||||
|
self.contract_allowed = Contract.objects.create(
|
||||||
|
company=self.company,
|
||||||
|
contract_number="CTR-ITS-001",
|
||||||
|
start_date=date(2025, 1, 1),
|
||||||
|
end_date=date(2030, 1, 1),
|
||||||
|
description="Contrat Maintenance Bornes"
|
||||||
|
)
|
||||||
|
self.contract_allowed.thematics.add(self.thematic_its)
|
||||||
|
|
||||||
|
UserContractAccess.objects.create(
|
||||||
|
user_config=self.cfg_external,
|
||||||
|
contract=self.contract_allowed,
|
||||||
|
can_view_assets=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.contract_other = Contract.objects.create(
|
||||||
|
company=self.company,
|
||||||
|
contract_number="CTR-ITS-002",
|
||||||
|
start_date=date(2025, 1, 1),
|
||||||
|
end_date=date(2030, 1, 1),
|
||||||
|
description="Contrat Autre"
|
||||||
|
)
|
||||||
|
self.contract_other.thematics.add(self.thematic_its)
|
||||||
|
|
||||||
|
# Base Location
|
||||||
|
from django.contrib.gis.geos import Polygon, MultiPolygon
|
||||||
|
center = Point(4.3517, 50.8400, srid=4326)
|
||||||
|
center.transform(3812)
|
||||||
|
poly = center.buffer(20)
|
||||||
|
self.location = ITSLocation.objects.create(
|
||||||
|
code="LOC-001",
|
||||||
|
name_fr="Rue Haute - Entrée Piétonne",
|
||||||
|
lon=4.3517,
|
||||||
|
lat=50.8400,
|
||||||
|
geom=MultiPolygon([poly], srid=3812)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bollard System 1 (with geometry LineString)
|
||||||
|
# 3812 coords approx around Brussels
|
||||||
|
line_3812 = LineString((148000, 170000), (148020, 170010), srid=3812)
|
||||||
|
self.system1 = ITSBollardSystem.objects.create(
|
||||||
|
code="BS-001",
|
||||||
|
name_fr="Traverse Bornes Rue Haute",
|
||||||
|
status="active",
|
||||||
|
width=6.5,
|
||||||
|
direction="bidirectional",
|
||||||
|
geom=line_3812,
|
||||||
|
lon=4.3517,
|
||||||
|
lat=50.8400,
|
||||||
|
location=self.location
|
||||||
|
)
|
||||||
|
ITSBollardSystemContract.objects.create(
|
||||||
|
system=self.system1,
|
||||||
|
contract=self.contract_allowed,
|
||||||
|
status="active"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bollard Plots for System 1
|
||||||
|
plot_pt1 = Point(148005, 170002, srid=3812)
|
||||||
|
self.plot1 = ITSBollardPlot.objects.create(
|
||||||
|
code="PL-001",
|
||||||
|
name_fr="Borne 1 (Gauche)",
|
||||||
|
system=self.system1,
|
||||||
|
position=1,
|
||||||
|
bollard_type="automatic",
|
||||||
|
status="active",
|
||||||
|
geom=plot_pt1,
|
||||||
|
lon=4.35172,
|
||||||
|
lat=50.84002
|
||||||
|
)
|
||||||
|
plot_pt2 = Point(148015, 170007, srid=3812)
|
||||||
|
self.plot2 = ITSBollardPlot.objects.create(
|
||||||
|
code="PL-002",
|
||||||
|
name_fr="Borne 2 (Droite)",
|
||||||
|
system=self.system1,
|
||||||
|
position=2,
|
||||||
|
bollard_type="automatic",
|
||||||
|
status="active",
|
||||||
|
geom=plot_pt2,
|
||||||
|
lon=4.35178,
|
||||||
|
lat=50.84005
|
||||||
|
)
|
||||||
|
|
||||||
|
# Linked Camera for System 1
|
||||||
|
from assets.models.its import ITSAssetModel
|
||||||
|
self.cam_model = ITSAssetModel.objects.create(
|
||||||
|
code="MOD-CAM-01",
|
||||||
|
name_fr="Caméra Axis LAPI"
|
||||||
|
)
|
||||||
|
cam_pt = Point(148010, 170015, srid=3812)
|
||||||
|
self.camera = ITSGeoAsset.objects.create(
|
||||||
|
model=self.cam_model,
|
||||||
|
code="CAM-001",
|
||||||
|
name_fr="Caméra LAPI Entrée",
|
||||||
|
status="active",
|
||||||
|
geom=cam_pt,
|
||||||
|
lon=4.35175,
|
||||||
|
lat=50.84010,
|
||||||
|
location=self.location
|
||||||
|
)
|
||||||
|
self.link_cam = ITSBollardSystemAsset.objects.create(
|
||||||
|
bollard_system=self.system1,
|
||||||
|
asset=self.camera,
|
||||||
|
role=ITSBollardSystemAsset.AssetRole.CAMERA_ENTRY,
|
||||||
|
comment="Caméra fixée sur candélabre"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bollard System 2 (Linked only to contract_other)
|
||||||
|
line2_3812 = LineString((149000, 171000), (149020, 171010), srid=3812)
|
||||||
|
self.system2 = ITSBollardSystem.objects.create(
|
||||||
|
code="BS-002",
|
||||||
|
name_fr="Traverse Bornes Boulevard",
|
||||||
|
status="active",
|
||||||
|
width=8.0,
|
||||||
|
direction="entry_only",
|
||||||
|
geom=line2_3812,
|
||||||
|
lon=4.3600,
|
||||||
|
lat=50.8500
|
||||||
|
)
|
||||||
|
ITSBollardSystemContract.objects.create(
|
||||||
|
system=self.system2,
|
||||||
|
contract=self.contract_other,
|
||||||
|
status="active"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bollard System 3 (Removed status - should be excluded)
|
||||||
|
line3_3812 = LineString((150000, 172000), (150020, 172010), srid=3812)
|
||||||
|
self.system_removed = ITSBollardSystem.objects.create(
|
||||||
|
code="BS-REMOVED",
|
||||||
|
name_fr="Traverse Supprimée",
|
||||||
|
status="removed",
|
||||||
|
geom=line3_3812,
|
||||||
|
lon=4.3700,
|
||||||
|
lat=50.8600
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def test_superuser_sees_all_active_assets(self):
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
url = reverse('assets:its_bollard_systems_geojson')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data.get('type'), 'FeatureCollection')
|
||||||
|
features = data.get('features', [])
|
||||||
|
codes = [f['properties']['code'] for f in features]
|
||||||
|
|
||||||
|
# Check system 1 & 2 are included, removed system is excluded
|
||||||
|
self.assertIn("BS-001", codes)
|
||||||
|
self.assertIn("BS-002", codes)
|
||||||
|
self.assertNotIn("BS-REMOVED", codes)
|
||||||
|
|
||||||
|
# Check plots are included
|
||||||
|
self.assertIn("PL-001", codes)
|
||||||
|
self.assertIn("PL-002", codes)
|
||||||
|
|
||||||
|
# Check camera is included
|
||||||
|
self.assertIn("CAM-001", codes)
|
||||||
|
|
||||||
|
# Check feature properties for system
|
||||||
|
bs1_feat = next(f for f in features if f['properties']['code'] == 'BS-001')
|
||||||
|
self.assertEqual(bs1_feat['geometry']['type'], 'LineString')
|
||||||
|
self.assertEqual(bs1_feat['properties']['asset_type'], 'itsbollardsystem')
|
||||||
|
self.assertEqual(bs1_feat['properties']['type'], 'asset')
|
||||||
|
self.assertEqual(bs1_feat['properties']['width'], 6.5)
|
||||||
|
self.assertEqual(bs1_feat['properties']['plots_count'], 2)
|
||||||
|
self.assertEqual(bs1_feat['properties']['location_id'], self.location.id)
|
||||||
|
|
||||||
|
# Check feature properties for plot
|
||||||
|
pl1_feat = next(f for f in features if f['properties']['code'] == 'PL-001')
|
||||||
|
self.assertEqual(pl1_feat['geometry']['type'], 'Point')
|
||||||
|
self.assertEqual(pl1_feat['properties']['asset_type'], 'itsbollardplot')
|
||||||
|
self.assertEqual(pl1_feat['properties']['type'], 'asset')
|
||||||
|
self.assertEqual(pl1_feat['properties']['position'], 1)
|
||||||
|
self.assertEqual(pl1_feat['properties']['system_code'], 'BS-001')
|
||||||
|
|
||||||
|
# Check feature properties for camera
|
||||||
|
cam_feat = next(f for f in features if f['properties']['code'] == 'CAM-001')
|
||||||
|
self.assertEqual(cam_feat['geometry']['type'], 'Point')
|
||||||
|
self.assertEqual(cam_feat['properties']['asset_type'], 'itsgeoasset')
|
||||||
|
self.assertTrue(cam_feat['properties']['is_camera'])
|
||||||
|
self.assertEqual(cam_feat['properties']['role'], 'camera_entry')
|
||||||
|
self.assertEqual(cam_feat['properties']['system_code'], 'BS-001')
|
||||||
|
|
||||||
|
def test_external_user_restricted_by_contracts(self):
|
||||||
|
self.client.force_login(self.user_external)
|
||||||
|
url = reverse('assets:its_bollard_systems_geojson')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
features = data.get('features', [])
|
||||||
|
codes = [f['properties']['code'] for f in features]
|
||||||
|
|
||||||
|
# External user should only see BS-001 and its plots/cameras
|
||||||
|
self.assertIn("BS-001", codes)
|
||||||
|
self.assertIn("PL-001", codes)
|
||||||
|
self.assertIn("CAM-001", codes)
|
||||||
|
|
||||||
|
# System 2 is not linked to their contract
|
||||||
|
self.assertNotIn("BS-002", codes)
|
||||||
|
|
||||||
|
def test_internal_user_can_view_all_active_systems(self):
|
||||||
|
self.client.force_login(self.user_internal)
|
||||||
|
url = reverse('assets:its_bollard_systems_geojson')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
features = data.get('features', [])
|
||||||
|
codes = [f['properties']['code'] for f in features]
|
||||||
|
|
||||||
|
self.assertIn("BS-001", codes)
|
||||||
|
self.assertIn("BS-002", codes)
|
||||||
|
self.assertNotIn("BS-REMOVED", codes)
|
||||||
|
|
||||||
|
def test_unauthenticated_user_forbidden(self):
|
||||||
|
url = reverse('assets:its_bollard_systems_geojson')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertIn(response.status_code, [302, 403])
|
||||||
|
|
||||||
|
def test_get_nearest_its_location(self):
|
||||||
|
self.client.force_login(self.user_internal)
|
||||||
|
url = reverse('assets:get_nearest_its_location')
|
||||||
|
response = self.client.get(url, {'lon': 4.3517, 'lat': 50.8400})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data.get('location_id'), self.location.id)
|
||||||
|
self.assertIn(self.location.code, data.get('location_label', ''))
|
||||||
|
|
||||||
|
def test_get_nearest_its_location_invalid_coords(self):
|
||||||
|
self.client.force_login(self.user_internal)
|
||||||
|
url = reverse('assets:get_nearest_its_location')
|
||||||
|
response = self.client.get(url, {'lon': 'invalid', 'lat': 'invalid'})
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
@ -217,11 +217,13 @@ urlpatterns = [
|
||||||
path('its/', its.its_index, name='its_index'),
|
path('its/', its.its_index, name='its_index'),
|
||||||
path('its/locations/', its.its_locations, name='its_locations'),
|
path('its/locations/', its.its_locations, name='its_locations'),
|
||||||
path('its/locations/geojson/', its.its_locations_geojson, name='its_locations_geojson'),
|
path('its/locations/geojson/', its.its_locations_geojson, name='its_locations_geojson'),
|
||||||
|
path('its/locations/get-nearest/', its.get_nearest_its_location, name='get_nearest_its_location'),
|
||||||
path('its/locations/<int:location_id>/', its.its_locations_detail, name='its_locations_detail'),
|
path('its/locations/<int:location_id>/', its.its_locations_detail, name='its_locations_detail'),
|
||||||
path('its/locations/<int:location_id>/add-document/', its.its_location_add_document, name='its_location_add_document'),
|
path('its/locations/<int:location_id>/add-document/', its.its_location_add_document, name='its_location_add_document'),
|
||||||
path('its/locations/<int:document_id>/delete-document/', its.its_location_delete_document, name='its_location_delete_document'),
|
path('its/locations/<int:document_id>/delete-document/', its.its_location_delete_document, name='its_location_delete_document'),
|
||||||
path('its/assets/', its.its_assets, name='its_assets'),
|
path('its/assets/', its.its_assets, name='its_assets'),
|
||||||
path('its/assets/geojson/', its.its_assets_geojson, name='its_assets_geojson'),
|
path('its/assets/geojson/', its.its_assets_geojson, name='its_assets_geojson'),
|
||||||
|
path('its/bollard-systems/geojson/', its.its_bollard_systems_geojson, name='its_bollard_systems_geojson'),
|
||||||
path('its/assets/autocomplete/', its.its_assets_autocomplete, name='its_autocomplete_assets'),
|
path('its/assets/autocomplete/', its.its_assets_autocomplete, name='its_autocomplete_assets'),
|
||||||
path('its/<str:asset_model>/<int:asset_id>/', its.its_assets_detail, name='its_assets_detail'),
|
path('its/<str:asset_model>/<int:asset_id>/', its.its_assets_detail, name='its_assets_detail'),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,263 @@ def its_assets_geojson(request):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@check_thematic_access('its')
|
||||||
|
@require_GET
|
||||||
|
def its_bollard_systems_geojson(request):
|
||||||
|
"""
|
||||||
|
Returns a GeoJSON FeatureCollection of retractable bollard system assets:
|
||||||
|
- LineString features for ITSBollardSystem
|
||||||
|
- Point features for ITSBollardPlot
|
||||||
|
- Point features for linked cameras (ITSGeoAsset linked via ITSBollardSystemAsset)
|
||||||
|
"""
|
||||||
|
bollard_system_ct = ContentType.objects.get_for_model(ITSBollardSystem)
|
||||||
|
bollard_plot_ct = ContentType.objects.get_for_model(ITSBollardPlot)
|
||||||
|
geoasset_ct = ContentType.objects.get_for_model(ITSGeoAsset)
|
||||||
|
location_ct = ContentType.objects.get_for_model(ITSLocation)
|
||||||
|
|
||||||
|
bollard_system_qs = (
|
||||||
|
ITSBollardSystem.objects
|
||||||
|
.select_related('location', 'category', 'model')
|
||||||
|
.prefetch_related('plots')
|
||||||
|
.exclude(status='removed')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Restriction par contrat pour utilisateurs externes
|
||||||
|
user = request.user
|
||||||
|
if user.is_authenticated:
|
||||||
|
try:
|
||||||
|
user_config = user.config
|
||||||
|
except UserConfig.DoesNotExist:
|
||||||
|
user_config = None
|
||||||
|
|
||||||
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
|
must_limit = is_external and (not user_config or user_config.limit_assets_to_contracts)
|
||||||
|
|
||||||
|
if must_limit:
|
||||||
|
accessible_contract_ids = UserContractAccess.objects.filter(
|
||||||
|
user_config=user_config,
|
||||||
|
can_view_assets=True
|
||||||
|
).values_list('contract_id', flat=True)
|
||||||
|
|
||||||
|
bollard_system_qs = bollard_system_qs.filter(
|
||||||
|
bollard_systems_contracts__status='active',
|
||||||
|
bollard_systems_contracts__contract_id__in=accessible_contract_ids
|
||||||
|
).distinct()
|
||||||
|
else:
|
||||||
|
bollard_system_qs = bollard_system_qs.none()
|
||||||
|
|
||||||
|
def _extract_geom_and_coords(obj):
|
||||||
|
geometry = None
|
||||||
|
if getattr(obj, 'geojson', None):
|
||||||
|
try:
|
||||||
|
geometry = json.loads(obj.geojson)
|
||||||
|
except Exception:
|
||||||
|
geometry = None
|
||||||
|
|
||||||
|
lon = getattr(obj, 'lon', None)
|
||||||
|
lat = getattr(obj, 'lat', None)
|
||||||
|
|
||||||
|
if (lon is None or lat is None or geometry is None) and getattr(obj, 'geom', None):
|
||||||
|
try:
|
||||||
|
g = obj.geom
|
||||||
|
if g.srid and g.srid != 4326:
|
||||||
|
g = g.transform(4326, clone=True)
|
||||||
|
if geometry is None:
|
||||||
|
geometry = json.loads(g.geojson)
|
||||||
|
coords = getattr(g, 'coords', None)
|
||||||
|
if coords:
|
||||||
|
if isinstance(coords[0], (int, float)):
|
||||||
|
if lon is None: lon = coords[0]
|
||||||
|
if lat is None: lat = coords[1]
|
||||||
|
elif isinstance(coords[0], (list, tuple)) and coords[0]:
|
||||||
|
mid = len(coords) // 2
|
||||||
|
if lon is None: lon = coords[mid][0]
|
||||||
|
if lat is None: lat = coords[mid][1]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if geometry is None and lon is not None and lat is not None:
|
||||||
|
geometry = {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [lon, lat]
|
||||||
|
}
|
||||||
|
|
||||||
|
return geometry, lon, lat
|
||||||
|
|
||||||
|
features = []
|
||||||
|
|
||||||
|
# 1. Bollard Systems (Lines)
|
||||||
|
for system in bollard_system_qs:
|
||||||
|
geometry, lon, lat = _extract_geom_and_coords(system)
|
||||||
|
if not geometry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
loc = system.location
|
||||||
|
loc_label = f"{loc.get_name()} ({loc.code})" if (loc and loc.code) else (loc.get_name() if loc else None)
|
||||||
|
label = f"{system.code} - {system.get_name()}" if system.get_name() else system.code
|
||||||
|
|
||||||
|
features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": system.id,
|
||||||
|
"geometry": geometry,
|
||||||
|
"properties": {
|
||||||
|
"type": "asset",
|
||||||
|
"asset_type": "itsbollardsystem",
|
||||||
|
"id": system.id,
|
||||||
|
"ct": bollard_system_ct.id,
|
||||||
|
"code": system.code,
|
||||||
|
"name": system.get_name(),
|
||||||
|
"label": label,
|
||||||
|
"equipment_type": _("Système de bornes"),
|
||||||
|
"status": system.status,
|
||||||
|
"width": system.width,
|
||||||
|
"direction": system.get_direction_display() if hasattr(system, 'get_direction_display') else system.direction,
|
||||||
|
"plots_count": system.plots.count(),
|
||||||
|
"lon": lon,
|
||||||
|
"lat": lat,
|
||||||
|
"location_id": loc.id if loc else None,
|
||||||
|
"location_ct": location_ct.id if loc else None,
|
||||||
|
"location_label": loc_label,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. Bollard Plots (Points)
|
||||||
|
bollard_plot_qs = (
|
||||||
|
ITSBollardPlot.objects
|
||||||
|
.filter(system__in=bollard_system_qs)
|
||||||
|
.select_related('system', 'location', 'category', 'model', 'system__location')
|
||||||
|
.exclude(status='removed')
|
||||||
|
)
|
||||||
|
|
||||||
|
for plot in bollard_plot_qs:
|
||||||
|
geometry, lon, lat = _extract_geom_and_coords(plot)
|
||||||
|
if not geometry and plot.system:
|
||||||
|
_ignored, lon, lat = _extract_geom_and_coords(plot.system)
|
||||||
|
if lon is not None and lat is not None:
|
||||||
|
geometry = {"type": "Point", "coordinates": [lon, lat]}
|
||||||
|
|
||||||
|
if not geometry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
loc = plot.location or (plot.system.location if plot.system else None)
|
||||||
|
loc_label = f"{loc.get_name()} ({loc.code})" if (loc and loc.code) else (loc.get_name() if loc else None)
|
||||||
|
plot_name = plot.get_name() or f"Plot #{plot.position}"
|
||||||
|
label = f"{plot.code} - {plot_name}" if plot.code else plot_name
|
||||||
|
|
||||||
|
features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": plot.id,
|
||||||
|
"geometry": geometry,
|
||||||
|
"properties": {
|
||||||
|
"type": "asset",
|
||||||
|
"asset_type": "itsbollardplot",
|
||||||
|
"id": plot.id,
|
||||||
|
"ct": bollard_plot_ct.id,
|
||||||
|
"code": plot.code,
|
||||||
|
"name": plot_name,
|
||||||
|
"label": label,
|
||||||
|
"equipment_type": plot.get_bollard_type_display() if hasattr(plot, 'get_bollard_type_display') else _("Plot de borne"),
|
||||||
|
"bollard_type": plot.bollard_type,
|
||||||
|
"bollard_type_display": plot.get_bollard_type_display() if hasattr(plot, 'get_bollard_type_display') else plot.bollard_type,
|
||||||
|
"status": plot.status,
|
||||||
|
"position": plot.position,
|
||||||
|
"system_id": plot.system_id,
|
||||||
|
"system_code": plot.system.code if plot.system else None,
|
||||||
|
"lon": lon,
|
||||||
|
"lat": lat,
|
||||||
|
"location_id": loc.id if loc else None,
|
||||||
|
"location_ct": location_ct.id if loc else None,
|
||||||
|
"location_label": loc_label,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Linked Cameras (Points)
|
||||||
|
camera_roles = [
|
||||||
|
ITSBollardSystemAsset.AssetRole.CAMERA_ENTRY,
|
||||||
|
ITSBollardSystemAsset.AssetRole.CAMERA_EXIT,
|
||||||
|
ITSBollardSystemAsset.AssetRole.CAMERA_CCTV,
|
||||||
|
]
|
||||||
|
system_assets_qs = (
|
||||||
|
ITSBollardSystemAsset.objects
|
||||||
|
.filter(bollard_system__in=bollard_system_qs)
|
||||||
|
.filter(
|
||||||
|
Q(role__in=camera_roles) |
|
||||||
|
Q(role__icontains='camera') |
|
||||||
|
Q(asset__category__name_fr__icontains='caméra') |
|
||||||
|
Q(asset__category__name_nl__icontains='camera') |
|
||||||
|
Q(asset__category__code__icontains='camera') |
|
||||||
|
Q(asset__model__name_fr__icontains='caméra') |
|
||||||
|
Q(asset__model__name_nl__icontains='camera') |
|
||||||
|
Q(asset__model__code__icontains='camera')
|
||||||
|
)
|
||||||
|
.select_related(
|
||||||
|
'asset', 'asset__model', 'asset__category', 'bollard_system',
|
||||||
|
'asset__location', 'bollard_system__location',
|
||||||
|
'asset__trafficlights_pole', 'asset__lighting_pole'
|
||||||
|
)
|
||||||
|
.exclude(asset__status='removed')
|
||||||
|
)
|
||||||
|
|
||||||
|
seen_camera_ids = set()
|
||||||
|
for link in system_assets_qs:
|
||||||
|
camera = link.asset
|
||||||
|
if camera.id in seen_camera_ids:
|
||||||
|
continue
|
||||||
|
seen_camera_ids.add(camera.id)
|
||||||
|
|
||||||
|
geometry, lon, lat = _extract_geom_and_coords(camera)
|
||||||
|
if not geometry:
|
||||||
|
support = camera.trafficlights_pole or camera.lighting_pole
|
||||||
|
if support:
|
||||||
|
geometry, lon, lat = _extract_geom_and_coords(support)
|
||||||
|
if not geometry and link.bollard_system:
|
||||||
|
_ignored, lon, lat = _extract_geom_and_coords(link.bollard_system)
|
||||||
|
if lon is not None and lat is not None:
|
||||||
|
geometry = {"type": "Point", "coordinates": [lon, lat]}
|
||||||
|
|
||||||
|
if not geometry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
loc = camera.location or (link.bollard_system.location if link.bollard_system else None)
|
||||||
|
loc_label = f"{loc.get_name()} ({loc.code})" if (loc and loc.code) else (loc.get_name() if loc else None)
|
||||||
|
cam_name = camera.get_name() or link.get_role_display()
|
||||||
|
label = f"{camera.code} - {cam_name}" if camera.code else cam_name
|
||||||
|
|
||||||
|
features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": camera.id,
|
||||||
|
"geometry": geometry,
|
||||||
|
"properties": {
|
||||||
|
"type": "asset",
|
||||||
|
"asset_type": "itsgeoasset",
|
||||||
|
"is_camera": True,
|
||||||
|
"id": camera.id,
|
||||||
|
"ct": geoasset_ct.id,
|
||||||
|
"code": camera.code,
|
||||||
|
"name": cam_name,
|
||||||
|
"label": label,
|
||||||
|
"equipment_type": link.get_role_display() or _("Caméra"),
|
||||||
|
"status": camera.status,
|
||||||
|
"role": link.role,
|
||||||
|
"role_display": link.get_role_display(),
|
||||||
|
"comment": link.comment,
|
||||||
|
"system_id": link.bollard_system_id,
|
||||||
|
"system_code": link.bollard_system.code if link.bollard_system else None,
|
||||||
|
"lon": lon,
|
||||||
|
"lat": lat,
|
||||||
|
"location_id": loc.id if loc else None,
|
||||||
|
"location_ct": location_ct.id if loc else None,
|
||||||
|
"location_label": loc_label,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": features,
|
||||||
|
}, safe=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@check_thematic_access('its')
|
@check_thematic_access('its')
|
||||||
def its_locations_detail(request, location_id):
|
def its_locations_detail(request, location_id):
|
||||||
# Préfetch (réduction N+1) : ASSETS + enfants (2 niveaux)
|
# Préfetch (réduction N+1) : ASSETS + enfants (2 niveaux)
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ function updateSelectedEquipmentsUI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyEquipmentLocation(item) {
|
function applyEquipmentLocation(item, options = {}) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +247,7 @@ function applyEquipmentLocation(item) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.lon && item.lat) {
|
if (!options.preserveCoordinates && item.lon && item.lat) {
|
||||||
$('#id_lon').val(item.lon);
|
$('#id_lon').val(item.lon);
|
||||||
$('#id_lat').val(item.lat);
|
$('#id_lat').val(item.lat);
|
||||||
centerMap();
|
centerMap();
|
||||||
|
|
@ -279,7 +279,7 @@ function addEquipmentSelection(item, options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedEquipmentsUI();
|
updateSelectedEquipmentsUI();
|
||||||
applyEquipmentLocation(item);
|
applyEquipmentLocation(item, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
const isSelected = selectedEquipments.some(s => s.equipment_ct == eq.ct && s.equipment_id == eq.id);
|
const isSelected = selectedEquipments.some(s => s.equipment_ct == eq.ct && s.equipment_id == eq.id);
|
||||||
html += `
|
html += `
|
||||||
<div class="form-check py-1 px-3">
|
<div class="form-check py-1 px-3">
|
||||||
<input class="form-check-input select-eq-cb" type="checkbox" id="eq-cb-${idx}" data-ct="${eq.ct}" data-id="${eq.id}" data-label="${eq.label}" data-lon="${eq.lon}" data-lat="${eq.lat}" ${isSelected ? 'checked' : ''}>
|
<input class="form-check-input select-eq-cb" type="checkbox" id="eq-cb-${idx}"
|
||||||
|
data-ct="${eq.ct}"
|
||||||
|
data-id="${eq.id}"
|
||||||
|
data-label="${eq.label}"
|
||||||
|
data-equipment-type="${eq.equipment_type || ''}"
|
||||||
|
data-location-id="${eq.location_id || ''}"
|
||||||
|
data-location-ct="${eq.location_ct || ''}"
|
||||||
|
data-location-label="${eq.location_label || ''}"
|
||||||
|
data-lon="${eq.lon}"
|
||||||
|
data-lat="${eq.lat}"
|
||||||
|
${isSelected ? 'checked' : ''}>
|
||||||
<label class="form-check-label ms-1 text-truncate" style="max-width: 180px; cursor: pointer; font-size: 0.85rem;" for="eq-cb-${idx}">
|
<label class="form-check-label ms-1 text-truncate" style="max-width: 180px; cursor: pointer; font-size: 0.85rem;" for="eq-cb-${idx}">
|
||||||
${eq.label}
|
${eq.label}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -252,11 +262,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
const item = {
|
const item = {
|
||||||
equipment_id: cb.dataset.id,
|
equipment_id: cb.dataset.id,
|
||||||
equipment_ct: cb.dataset.ct,
|
equipment_ct: cb.dataset.ct,
|
||||||
|
equipment_type: cb.dataset.equipmentType || '',
|
||||||
label: cb.dataset.label,
|
label: cb.dataset.label,
|
||||||
|
location_id: cb.dataset.locationId || null,
|
||||||
|
location_ct: cb.dataset.locationCt || null,
|
||||||
|
location_label: cb.dataset.locationLabel || null,
|
||||||
lon: parseFloat(cb.dataset.lon),
|
lon: parseFloat(cb.dataset.lon),
|
||||||
lat: parseFloat(cb.dataset.lat)
|
lat: parseFloat(cb.dataset.lat)
|
||||||
};
|
};
|
||||||
addEquipmentSelection(item, { replaceExisting: false });
|
addEquipmentSelection(item, { replaceExisting: false, preserveCoordinates: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// NOTE: marker stays at click position — not moved to any equipment.
|
// NOTE: marker stays at click position — not moved to any equipment.
|
||||||
|
|
@ -330,8 +344,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
'traffic-lights',
|
'traffic-lights',
|
||||||
'controlcenters',
|
'controlcenters',
|
||||||
'fountains',
|
'fountains',
|
||||||
'monuments'
|
'monuments',
|
||||||
].filter(layerId => map.getLayer(layerId));
|
'its-bollard-systems-line',
|
||||||
|
'its-bollard-plots-circle',
|
||||||
|
'its-cameras-circle'
|
||||||
|
].filter(layerId => {
|
||||||
|
if (!map.getLayer(layerId)) return false;
|
||||||
|
try {
|
||||||
|
return map.getLayoutProperty(layerId, 'visibility') !== 'none';
|
||||||
|
} catch (err) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const bbox = [
|
const bbox = [
|
||||||
[e.point.x - 5, e.point.y - 5],
|
[e.point.x - 5, e.point.y - 5],
|
||||||
|
|
@ -346,13 +370,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
clickedFeatures.forEach(feat => {
|
clickedFeatures.forEach(feat => {
|
||||||
const props = feat.properties || {};
|
const props = feat.properties || {};
|
||||||
const type = props.type; // 'asset' or 'location' for inspection-zones
|
const type = props.type; // 'asset' or 'location'
|
||||||
|
|
||||||
// 1. From our custom inspection configuration layers
|
// 1. From our custom inspection configuration layers or ITS bollard layers
|
||||||
if (feat.layer.id.startsWith('inspection-zones')) {
|
if (feat.layer.id.startsWith('inspection-zones') || feat.layer.id.startsWith('its-') || type === 'asset') {
|
||||||
const ct = props.ct;
|
const ct = props.ct;
|
||||||
const id = props.id;
|
const id = props.id || feat.id;
|
||||||
const label = props.label || props.name || props.code || `${type} ${id}`;
|
const label = props.label || props.name || props.code || `${type || 'asset'} ${id}`;
|
||||||
let lon = props.lon ? parseFloat(props.lon) : null;
|
let lon = props.lon ? parseFloat(props.lon) : null;
|
||||||
let lat = props.lat ? parseFloat(props.lat) : null;
|
let lat = props.lat ? parseFloat(props.lat) : null;
|
||||||
|
|
||||||
|
|
@ -366,11 +390,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'asset' && ct && id) {
|
if ((type === 'asset' || feat.layer.id.startsWith('its-')) && ct && id) {
|
||||||
const key = `${ct}:${id}`;
|
const key = `${ct}:${id}`;
|
||||||
if (!seenEquip.has(key)) {
|
if (!seenEquip.has(key)) {
|
||||||
seenEquip.add(key);
|
seenEquip.add(key);
|
||||||
equipments.push({ ct: ct, id: id, label: label, lon: lon, lat: lat });
|
equipments.push({
|
||||||
|
ct: ct,
|
||||||
|
id: id,
|
||||||
|
label: label,
|
||||||
|
equipment_type: props.equipment_type || '',
|
||||||
|
location_id: props.location_id || null,
|
||||||
|
location_ct: props.location_ct || null,
|
||||||
|
location_label: props.location_label || null,
|
||||||
|
lon: lon,
|
||||||
|
lat: lat
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (type === 'location' && ct && id) {
|
} else if (type === 'location' && ct && id) {
|
||||||
const key = `${ct}:${id}`;
|
const key = `${ct}:${id}`;
|
||||||
|
|
@ -423,10 +457,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
addEquipmentSelection({
|
addEquipmentSelection({
|
||||||
equipment_id: eq.id,
|
equipment_id: eq.id,
|
||||||
equipment_ct: eq.ct,
|
equipment_ct: eq.ct,
|
||||||
|
equipment_type: eq.equipment_type || '',
|
||||||
label: eq.label,
|
label: eq.label,
|
||||||
|
location_id: eq.location_id,
|
||||||
|
location_ct: eq.location_ct,
|
||||||
|
location_label: eq.location_label,
|
||||||
lon: eq.lon,
|
lon: eq.lon,
|
||||||
lat: eq.lat
|
lat: eq.lat
|
||||||
}, { replaceExisting: true });
|
}, { replaceExisting: true, preserveCoordinates: true });
|
||||||
// NOTE: marker stays at click position — not moved to equipment coordinates.
|
// NOTE: marker stays at click position — not moved to equipment coordinates.
|
||||||
}
|
}
|
||||||
// 2) Si plusieurs équipements sont présents, popup checkboxes
|
// 2) Si plusieurs équipements sont présents, popup checkboxes
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
var autocomplete_assets_url = "{% url 'assets:sign_autocomplete_assets' %}";
|
var autocomplete_assets_url = "{% url 'assets:sign_autocomplete_assets' %}";
|
||||||
{% elif thematic.code == 'its' %}
|
{% elif thematic.code == 'its' %}
|
||||||
var autocomplete_assets_url = "{% url 'assets:its_autocomplete_assets' %}";
|
var autocomplete_assets_url = "{% url 'assets:its_autocomplete_assets' %}";
|
||||||
|
var its_bollard_systems_geojson_url = "{% url 'assets:its_bollard_systems_geojson' %}";
|
||||||
{% elif thematic.code == 'clean' %}
|
{% elif thematic.code == 'clean' %}
|
||||||
var autocomplete_assets_url = "{% url 'assets:clean_autocomplete_assets' %}";
|
var autocomplete_assets_url = "{% url 'assets:clean_autocomplete_assets' %}";
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -362,6 +363,308 @@
|
||||||
map.on('load', setupArtworks);
|
map.on('load', setupArtworks);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{% elif thematic.code == 'its' %}
|
||||||
|
<script>
|
||||||
|
const setupITSBollardSystems = async () => {
|
||||||
|
if (map.getSource('its-bollards')) return;
|
||||||
|
|
||||||
|
const geojsonUrl = typeof its_bollard_systems_geojson_url !== 'undefined'
|
||||||
|
? its_bollard_systems_geojson_url
|
||||||
|
: "{% url 'assets:its_bollard_systems_geojson' %}";
|
||||||
|
|
||||||
|
let geojsonData = { type: 'FeatureCollection', features: [] };
|
||||||
|
try {
|
||||||
|
const resp = await fetch(geojsonUrl);
|
||||||
|
if (resp.ok) {
|
||||||
|
geojsonData = await resp.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur lors du chargement des bornes ITS:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.addSource('its-bollards', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: geojsonData
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Casing / contour pour la ligne du système de bornes
|
||||||
|
map.addLayer({
|
||||||
|
id: 'its-bollard-systems-casing',
|
||||||
|
type: 'line',
|
||||||
|
source: 'its-bollards',
|
||||||
|
filter: ['==', ['get', 'asset_type'], 'itsbollardsystem'],
|
||||||
|
layout: {
|
||||||
|
'line-cap': 'round',
|
||||||
|
'line-join': 'round',
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'line-color': '#ffffff',
|
||||||
|
'line-width': 8,
|
||||||
|
'line-opacity': 0.8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Ligne du système de bornes (traverses)
|
||||||
|
map.addLayer({
|
||||||
|
id: 'its-bollard-systems-line',
|
||||||
|
type: 'line',
|
||||||
|
source: 'its-bollards',
|
||||||
|
filter: ['==', ['get', 'asset_type'], 'itsbollardsystem'],
|
||||||
|
layout: {
|
||||||
|
'line-cap': 'round',
|
||||||
|
'line-join': 'round',
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'line-color': '#0288d1',
|
||||||
|
'line-width': 5,
|
||||||
|
'line-opacity': 0.95
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Labels pour les systèmes de bornes (lignes)
|
||||||
|
map.addLayer({
|
||||||
|
id: 'its-bollard-systems-labels',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'its-bollards',
|
||||||
|
filter: ['==', ['get', 'asset_type'], 'itsbollardsystem'],
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'code'],
|
||||||
|
'text-font': ['Noto Sans Regular'],
|
||||||
|
'text-size': 12,
|
||||||
|
'symbol-placement': 'line-center',
|
||||||
|
'text-offset': [0, -1],
|
||||||
|
'text-anchor': 'bottom',
|
||||||
|
'text-allow-overlap': false,
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#01579b',
|
||||||
|
'text-halo-color': '#ffffff',
|
||||||
|
'text-halo-width': 2
|
||||||
|
},
|
||||||
|
minzoom: 12
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Points pour les plots / bornes
|
||||||
|
map.addLayer({
|
||||||
|
id: 'its-bollard-plots-circle',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'its-bollards',
|
||||||
|
filter: ['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
|
layout: {
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'circle-radius': [
|
||||||
|
'interpolate', ['linear'], ['zoom'],
|
||||||
|
12, 4,
|
||||||
|
15, 7,
|
||||||
|
18, 10
|
||||||
|
],
|
||||||
|
'circle-color': '#0288d1',
|
||||||
|
'circle-stroke-width': 2,
|
||||||
|
'circle-stroke-color': '#ffffff',
|
||||||
|
'circle-opacity': 0.95
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Labels pour les plots / bornes
|
||||||
|
map.addLayer({
|
||||||
|
id: 'its-bollard-plots-labels',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'its-bollards',
|
||||||
|
filter: ['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'code'],
|
||||||
|
'text-font': ['Noto Sans Regular'],
|
||||||
|
'text-size': 11,
|
||||||
|
'text-offset': [0, 1.3],
|
||||||
|
'text-anchor': 'top',
|
||||||
|
'text-allow-overlap': false,
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#01579b',
|
||||||
|
'text-halo-color': '#ffffff',
|
||||||
|
'text-halo-width': 2
|
||||||
|
},
|
||||||
|
minzoom: 13
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Points pour les caméras liées
|
||||||
|
map.addLayer({
|
||||||
|
id: 'its-cameras-circle',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'its-bollards',
|
||||||
|
filter: ['==', ['get', 'asset_type'], 'itsgeoasset'],
|
||||||
|
layout: {
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'circle-radius': [
|
||||||
|
'interpolate', ['linear'], ['zoom'],
|
||||||
|
12, 5,
|
||||||
|
15, 8,
|
||||||
|
18, 11
|
||||||
|
],
|
||||||
|
'circle-color': '#e65100',
|
||||||
|
'circle-stroke-width': 2,
|
||||||
|
'circle-stroke-color': '#ffffff',
|
||||||
|
'circle-opacity': 0.95
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Labels pour les caméras liées
|
||||||
|
map.addLayer({
|
||||||
|
id: 'its-cameras-labels',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'its-bollards',
|
||||||
|
filter: ['==', ['get', 'asset_type'], 'itsgeoasset'],
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'code'],
|
||||||
|
'text-font': ['Noto Sans Regular'],
|
||||||
|
'text-size': 11,
|
||||||
|
'text-offset': [0, 1.3],
|
||||||
|
'text-anchor': 'top',
|
||||||
|
'text-allow-overlap': false,
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#bf360c',
|
||||||
|
'text-halo-color': '#ffffff',
|
||||||
|
'text-halo-width': 2
|
||||||
|
},
|
||||||
|
minzoom: 13
|
||||||
|
});
|
||||||
|
|
||||||
|
// Curseur pointer au survol
|
||||||
|
const interactiveLayers = ['its-bollard-systems-line', 'its-bollard-plots-circle', 'its-cameras-circle'];
|
||||||
|
interactiveLayers.forEach(lId => {
|
||||||
|
map.on('mouseenter', lId, () => { map.getCanvas().style.cursor = 'pointer'; });
|
||||||
|
map.on('mouseleave', lId, () => { map.getCanvas().style.cursor = ''; });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tooltip / Popup au survol
|
||||||
|
const hoverPopup = new maplibregl.Popup({
|
||||||
|
closeButton: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
offset: 12
|
||||||
|
});
|
||||||
|
|
||||||
|
interactiveLayers.forEach(lId => {
|
||||||
|
map.on('mousemove', lId, (e) => {
|
||||||
|
if (!e.features || !e.features.length) return;
|
||||||
|
const feat = e.features[0];
|
||||||
|
const p = feat.properties;
|
||||||
|
let title = p.label || p.code || '';
|
||||||
|
let subtitle = p.equipment_type || '';
|
||||||
|
if (p.system_code && p.asset_type !== 'itsbollardsystem') {
|
||||||
|
subtitle += ` (${gettext('Système')}: ${p.system_code})`;
|
||||||
|
}
|
||||||
|
hoverPopup.setLngLat(e.lngLat)
|
||||||
|
.setHTML(`<div style="padding: 2px 4px; font-size: 0.85rem;"><strong>${title}</strong><br><span class="text-muted">${subtitle}</span></div>`)
|
||||||
|
.addTo(map);
|
||||||
|
});
|
||||||
|
map.on('mouseleave', lId, () => {
|
||||||
|
hoverPopup.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestion de la visibilité selon la catégorie sélectionnée
|
||||||
|
const itsLayers = [
|
||||||
|
'its-bollard-systems-casing',
|
||||||
|
'its-bollard-systems-line',
|
||||||
|
'its-bollard-systems-labels',
|
||||||
|
'its-bollard-plots-circle',
|
||||||
|
'its-bollard-plots-labels',
|
||||||
|
'its-cameras-circle',
|
||||||
|
'its-cameras-labels'
|
||||||
|
];
|
||||||
|
|
||||||
|
function isBollardsCategorySelected() {
|
||||||
|
const catSelect = document.getElementById('id_asset_category');
|
||||||
|
const subCatSelect = document.getElementById('id_interv_sub_category');
|
||||||
|
const check = (opt) => {
|
||||||
|
if (!opt || !opt.value) return false;
|
||||||
|
const txt = (opt.textContent || opt.text || '').toLowerCase();
|
||||||
|
return txt.includes('borne') || txt.includes('bollard') || txt.includes('rétractable') || txt.includes('escamotable') || txt.includes('poller') || txt.includes('paal');
|
||||||
|
};
|
||||||
|
if (subCatSelect && subCatSelect.selectedIndex >= 0 && check(subCatSelect.options[subCatSelect.selectedIndex])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (catSelect && catSelect.selectedIndex >= 0 && check(catSelect.options[catSelect.selectedIndex])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasFittedBounds = false;
|
||||||
|
function updateITSBollardsVisibility() {
|
||||||
|
const isVisible = isBollardsCategorySelected();
|
||||||
|
itsLayers.forEach(lId => {
|
||||||
|
if (map.getLayer(lId)) {
|
||||||
|
map.setLayoutProperty(lId, 'visibility', isVisible ? 'visible' : 'none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si rendu visible et pas de coordonnées existantes sur le formulaire, centrer sur les assets
|
||||||
|
if (isVisible && !hasFittedBounds && geojsonData.features && geojsonData.features.length > 0) {
|
||||||
|
const latVal = parseFloat(document.getElementById('id_lat')?.value);
|
||||||
|
const lonVal = parseFloat(document.getElementById('id_lon')?.value);
|
||||||
|
if (isNaN(latVal) || isNaN(lonVal)) {
|
||||||
|
const bounds = new maplibregl.LngLatBounds();
|
||||||
|
geojsonData.features.forEach(f => {
|
||||||
|
if (f.geometry && f.geometry.type === 'Point' && f.geometry.coordinates) {
|
||||||
|
bounds.extend(f.geometry.coordinates);
|
||||||
|
} else if (f.geometry && f.geometry.type === 'LineString' && f.geometry.coordinates) {
|
||||||
|
f.geometry.coordinates.forEach(c => bounds.extend(c));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!bounds.isEmpty()) {
|
||||||
|
map.fitBounds(bounds, { padding: 60, maxZoom: 16 });
|
||||||
|
hasFittedBounds = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Écouter les changements sur la catégorie principale et sous-catégorie
|
||||||
|
const catSelect = document.getElementById('id_asset_category');
|
||||||
|
if (catSelect) {
|
||||||
|
catSelect.addEventListener('change', updateITSBollardsVisibility);
|
||||||
|
}
|
||||||
|
const subCatSelect = document.getElementById('id_interv_sub_category');
|
||||||
|
if (subCatSelect) {
|
||||||
|
subCatSelect.addEventListener('change', updateITSBollardsVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observer les mutations éventuelles du bloc de sous-catégorie
|
||||||
|
const subCatBlock = document.getElementById('interv-sub-category-block');
|
||||||
|
if (subCatBlock) {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
updateITSBollardsVisibility();
|
||||||
|
const newSub = document.getElementById('id_interv_sub_category');
|
||||||
|
if (newSub) {
|
||||||
|
newSub.removeEventListener('change', updateITSBollardsVisibility);
|
||||||
|
newSub.addEventListener('change', updateITSBollardsVisibility);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(subCatBlock, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification initiale
|
||||||
|
updateITSBollardsVisibility();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (map.loaded()) {
|
||||||
|
setupITSBollardSystems();
|
||||||
|
} else {
|
||||||
|
map.on('load', setupITSBollardSystems);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue