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/locations/', its.its_locations, name='its_locations'),
|
||||
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>/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/assets/', its.its_assets, name='its_assets'),
|
||||
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/<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')
|
||||
def its_locations_detail(request, location_id):
|
||||
# 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) {
|
||||
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_lat').val(item.lat);
|
||||
centerMap();
|
||||
|
|
@ -279,7 +279,7 @@ function addEquipmentSelection(item, options = {}) {
|
|||
}
|
||||
|
||||
updateSelectedEquipmentsUI();
|
||||
applyEquipmentLocation(item);
|
||||
applyEquipmentLocation(item, options);
|
||||
}
|
||||
|
||||
$(function() {
|
||||
|
|
|
|||
|
|
@ -222,7 +222,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
const isSelected = selectedEquipments.some(s => s.equipment_ct == eq.ct && s.equipment_id == eq.id);
|
||||
html += `
|
||||
<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}">
|
||||
${eq.label}
|
||||
</label>
|
||||
|
|
@ -252,11 +262,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
const item = {
|
||||
equipment_id: cb.dataset.id,
|
||||
equipment_ct: cb.dataset.ct,
|
||||
equipment_type: cb.dataset.equipmentType || '',
|
||||
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),
|
||||
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.
|
||||
|
|
@ -330,8 +344,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
'traffic-lights',
|
||||
'controlcenters',
|
||||
'fountains',
|
||||
'monuments'
|
||||
].filter(layerId => map.getLayer(layerId));
|
||||
'monuments',
|
||||
'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 = [
|
||||
[e.point.x - 5, e.point.y - 5],
|
||||
|
|
@ -346,13 +370,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
|
||||
clickedFeatures.forEach(feat => {
|
||||
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
|
||||
if (feat.layer.id.startsWith('inspection-zones')) {
|
||||
// 1. From our custom inspection configuration layers or ITS bollard layers
|
||||
if (feat.layer.id.startsWith('inspection-zones') || feat.layer.id.startsWith('its-') || type === 'asset') {
|
||||
const ct = props.ct;
|
||||
const id = props.id;
|
||||
const label = props.label || props.name || props.code || `${type} ${id}`;
|
||||
const id = props.id || feat.id;
|
||||
const label = props.label || props.name || props.code || `${type || 'asset'} ${id}`;
|
||||
let lon = props.lon ? parseFloat(props.lon) : 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}`;
|
||||
if (!seenEquip.has(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) {
|
||||
const key = `${ct}:${id}`;
|
||||
|
|
@ -423,10 +457,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
addEquipmentSelection({
|
||||
equipment_id: eq.id,
|
||||
equipment_ct: eq.ct,
|
||||
equipment_type: eq.equipment_type || '',
|
||||
label: eq.label,
|
||||
location_id: eq.location_id,
|
||||
location_ct: eq.location_ct,
|
||||
location_label: eq.location_label,
|
||||
lon: eq.lon,
|
||||
lat: eq.lat
|
||||
}, { replaceExisting: true });
|
||||
}, { replaceExisting: true, preserveCoordinates: true });
|
||||
// NOTE: marker stays at click position — not moved to equipment coordinates.
|
||||
}
|
||||
// 2) Si plusieurs équipements sont présents, popup checkboxes
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
var autocomplete_assets_url = "{% url 'assets:sign_autocomplete_assets' %}";
|
||||
{% elif thematic.code == 'its' %}
|
||||
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' %}
|
||||
var autocomplete_assets_url = "{% url 'assets:clean_autocomplete_assets' %}";
|
||||
{% else %}
|
||||
|
|
@ -362,6 +363,308 @@
|
|||
map.on('load', setupArtworks);
|
||||
}
|
||||
</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 %}
|
||||
|
||||
<script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue