feat: extend support infrastructure for sign panels with traffic light pole association and custom model fields
This commit is contained in:
parent
48d7796e45
commit
1b38a8059e
11 changed files with 947 additions and 51 deletions
|
|
@ -8,17 +8,24 @@ class SignPoleForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SignPole
|
model = SignPole
|
||||||
fields = ['model', 'organization', 'orientation']
|
fields = ['category', 'model', 'organization', 'orientation', 'support_type']
|
||||||
widgets = {
|
widgets = {
|
||||||
'category': forms.Select(attrs={'class': 'form-select'}),
|
'category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'model': forms.Select(attrs={'class': 'form-select'}),
|
'model': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'organization': forms.Select(attrs={'class': 'form-select'}),
|
'organization': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'support_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'orientation': forms.HiddenInput(),
|
'orientation': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['organization'].required = True
|
self.fields['organization'].required = True
|
||||||
|
if 'support_type' in self.fields:
|
||||||
|
self.fields['support_type'].required = False
|
||||||
|
self.fields['support_type'].initial = 'pole'
|
||||||
|
|
||||||
|
def clean_support_type(self):
|
||||||
|
return self.cleaned_data.get('support_type') or 'pole'
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
instance = super().save(commit=False)
|
instance = super().save(commit=False)
|
||||||
|
|
@ -40,13 +47,14 @@ class SignPanelForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SignPanel
|
model = SignPanel
|
||||||
fields = ['signpanel_type', 'model', 'organization', 'orientation', 'support_type', 'is_temporary', 'signpanel_text', 'signpanel_value']
|
fields = ['signpanel_type', 'model', 'organization', 'orientation', 'support_type', 'support_model', 'is_temporary', 'signpanel_text', 'signpanel_value']
|
||||||
widgets = {
|
widgets = {
|
||||||
'signpanel_type': forms.Select(attrs={'class': 'form-select'}),
|
'signpanel_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'model': forms.Select(attrs={'class': 'form-select'}),
|
'model': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'organization': forms.Select(attrs={'class': 'form-select'}),
|
'organization': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'orientation': forms.NumberInput(attrs={'class': 'form-control'}),
|
'orientation': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
'support_type': forms.Select(attrs={'class': 'form-select'}),
|
'support_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'support_model': forms.TextInput(attrs={'class': 'form-control', 'placeholder': "Modèle du support (ex: Poteau Acier 3m, Feu tricolore carrefour...)"}),
|
||||||
'signpanel_text': forms.TextInput(attrs={'class': 'form-control'}),
|
'signpanel_text': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'signpanel_value': forms.NumberInput(attrs={'class': 'form-control'}),
|
'signpanel_value': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
'is_temporary': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
'is_temporary': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
|
|
@ -55,6 +63,11 @@ class SignPanelForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['organization'].required = True
|
self.fields['organization'].required = True
|
||||||
|
if 'support_type' in self.fields:
|
||||||
|
self.fields['support_type'].required = False
|
||||||
|
self.fields['support_type'].initial = 'pole'
|
||||||
|
if 'support_model' in self.fields:
|
||||||
|
self.fields['support_model'].required = False
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
instance = super().save(commit=False)
|
instance = super().save(commit=False)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-11 14:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0025_artworkfountain_external_reference_and_more'),
|
||||||
|
('sign', '0003_signpanel_external_reference_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='signpanel',
|
||||||
|
name='support_model',
|
||||||
|
field=models.CharField(blank=True, help_text='Modèle du support (ex: Poteau Acier 3m, Feu tricolore carrefour, etc.)', max_length=255, null=True, verbose_name='Modèle du support'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='signpanel',
|
||||||
|
name='trafficlight_pole',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sign_app_panels', to='assets.trafficlightpole', verbose_name='Feu tricolore associé'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='signpole',
|
||||||
|
name='support_type',
|
||||||
|
field=models.CharField(choices=[('pole', 'Poteau'), ('lightingpole', "Poteau d'éclairage"), ('trafficlightpole', 'Poteau de signalisation lumineuse'), ('facade', 'Façade'), ('mast', 'Mât'), ('portico', 'Portique'), ('gallows', 'Potence'), ('hopper', 'Trémie'), ('other', 'Autre')], default='pole', max_length=50, verbose_name='Type de support'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='signpanel',
|
||||||
|
name='signpanel_text',
|
||||||
|
field=models.CharField(blank=True, max_length=1000, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -141,6 +141,19 @@ class SignPoleModel(AbstractAssetModel):
|
||||||
verbose_name_plural = "Modèles de poteaux (Sign)"
|
verbose_name_plural = "Modèles de poteaux (Sign)"
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORT_TYPE_CHOICES = (
|
||||||
|
("pole", "Poteau"),
|
||||||
|
("lightingpole", "Poteau d'éclairage"),
|
||||||
|
("trafficlightpole", "Poteau de signalisation lumineuse"),
|
||||||
|
("facade", "Façade"),
|
||||||
|
("mast", "Mât"),
|
||||||
|
("portico", "Portique"),
|
||||||
|
("gallows", "Potence"),
|
||||||
|
("hopper", "Trémie"),
|
||||||
|
("other", "Autre"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SignPole(AbstractGeoAsset):
|
class SignPole(AbstractGeoAsset):
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
'assets.AssetCategory',
|
'assets.AssetCategory',
|
||||||
|
|
@ -160,10 +173,21 @@ class SignPole(AbstractGeoAsset):
|
||||||
default=90,
|
default=90,
|
||||||
help_text="Orientation en degrés (0-360)"
|
help_text="Orientation en degrés (0-360)"
|
||||||
)
|
)
|
||||||
|
support_type = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
default="pole",
|
||||||
|
blank=True,
|
||||||
|
choices=SUPPORT_TYPE_CHOICES,
|
||||||
|
verbose_name="Type de support",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(fields=["code"])]
|
indexes = [models.Index(fields=["code"])]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def support_type_display(self):
|
||||||
|
return dict(SUPPORT_TYPE_CHOICES).get(self.support_type, self.support_type or "Poteau")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.geom and hasattr(self, 'sign_app_panels'):
|
if self.geom and hasattr(self, 'sign_app_panels'):
|
||||||
|
|
@ -227,19 +251,6 @@ class SignPanelType(models.Model):
|
||||||
return f"{self.code} - {self.get_name()}"
|
return f"{self.code} - {self.get_name()}"
|
||||||
|
|
||||||
|
|
||||||
SUPPORT_TYPE_CHOICES = (
|
|
||||||
("pole", "Poteau"),
|
|
||||||
("lightingpole", "Poteau d'éclairage"),
|
|
||||||
("trafficlightpole", "Poteau de signalisation lumineuse"),
|
|
||||||
("facade", "Façade"),
|
|
||||||
("mast", "Mât"),
|
|
||||||
("portico", "Portique"),
|
|
||||||
("gallows", "Potence"),
|
|
||||||
("hopper", "Trémie"),
|
|
||||||
("other", "Autre"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SignPanel(AbstractGeoAsset):
|
class SignPanel(AbstractGeoAsset):
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
'assets.AssetCategory',
|
'assets.AssetCategory',
|
||||||
|
|
@ -251,11 +262,26 @@ class SignPanel(AbstractGeoAsset):
|
||||||
model = models.ForeignKey(SignPanelModel, on_delete=models.SET_NULL, null=True, blank=True)
|
model = models.ForeignKey(SignPanelModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
signpanel_type = models.ForeignKey(SignPanelType, on_delete=models.SET_NULL, null=True, blank=True)
|
signpanel_type = models.ForeignKey(SignPanelType, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
pole = models.ForeignKey(SignPole, related_name="sign_app_panels", on_delete=models.SET_NULL, null=True, blank=True)
|
pole = models.ForeignKey(SignPole, related_name="sign_app_panels", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
trafficlight_pole = models.ForeignKey(
|
||||||
|
"assets.TrafficLightPole",
|
||||||
|
related_name="sign_app_panels",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Feu tricolore associé",
|
||||||
|
)
|
||||||
vertical_order = models.IntegerField(null=True, blank=True)
|
vertical_order = models.IntegerField(null=True, blank=True)
|
||||||
signpanel_text = models.CharField(max_length=1000, null=True, blank=True, choices=SUPPORT_TYPE_CHOICES)
|
signpanel_text = models.CharField(max_length=1000, null=True, blank=True)
|
||||||
signpanel_value = models.FloatField(null=True, blank=True)
|
signpanel_value = models.FloatField(null=True, blank=True)
|
||||||
orientation = models.IntegerField(null=True, blank=True, help_text="Orientation en degrés (0-360)")
|
orientation = models.IntegerField(null=True, blank=True, help_text="Orientation en degrés (0-360)")
|
||||||
support_type = models.CharField(max_length=200, null=True, blank=True, choices=SUPPORT_TYPE_CHOICES)
|
support_type = models.CharField(max_length=200, null=True, blank=True, choices=SUPPORT_TYPE_CHOICES)
|
||||||
|
support_model = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Modèle du support",
|
||||||
|
help_text="Modèle du support (ex: Poteau Acier 3m, Feu tricolore carrefour, etc.)",
|
||||||
|
)
|
||||||
is_temporary = models.BooleanField(default=False)
|
is_temporary = models.BooleanField(default=False)
|
||||||
street = models.ForeignKey("common.AdrStreet", related_name="sign_app_panels", on_delete=models.SET_NULL, null=True, blank=True)
|
street = models.ForeignKey("common.AdrStreet", related_name="sign_app_panels", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
organization = models.ForeignKey(
|
organization = models.ForeignKey(
|
||||||
|
|
@ -267,22 +293,49 @@ class SignPanel(AbstractGeoAsset):
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(fields=["code"])]
|
indexes = [models.Index(fields=["code"])]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_support_type(self):
|
||||||
|
if self.support_type:
|
||||||
|
return self.support_type
|
||||||
|
if self.pole:
|
||||||
|
return getattr(self.pole, 'support_type', 'pole') or 'pole'
|
||||||
|
if self.trafficlight_pole:
|
||||||
|
return 'trafficlightpole'
|
||||||
|
return 'pole'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_support_type_display(self):
|
||||||
|
st = self.effective_support_type
|
||||||
|
return dict(SUPPORT_TYPE_CHOICES).get(st, st or "Poteau")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def support_model_display(self):
|
||||||
|
if self.support_model:
|
||||||
|
return self.support_model
|
||||||
|
if self.pole and self.pole.model:
|
||||||
|
return self.pole.model.get_name()
|
||||||
|
if self.trafficlight_pole and self.trafficlight_pole.model:
|
||||||
|
return self.trafficlight_pole.model.get_name()
|
||||||
|
return None
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
if self.pole and getattr(self.pole, 'geom', None):
|
support = self.pole or self.trafficlight_pole
|
||||||
self.geom = self.pole.geom
|
if support and getattr(support, 'geom', None):
|
||||||
self.lon = self.pole.lon
|
self.geom = support.geom
|
||||||
self.lat = self.pole.lat
|
self.lon = support.lon
|
||||||
self.geojson = self.pole.geojson
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
if self.pole and self.pole.street and not self.street_id:
|
if self.pole and self.pole.street and not self.street_id:
|
||||||
self.street = self.pole.street
|
self.street = self.pole.street
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.pole and getattr(self.pole, 'geom', None):
|
support = self.pole or self.trafficlight_pole
|
||||||
self.geom = self.pole.geom
|
if support and getattr(support, 'geom', None):
|
||||||
self.lon = self.pole.lon
|
self.geom = support.geom
|
||||||
self.lat = self.pole.lat
|
self.lon = support.lon
|
||||||
self.geojson = self.pole.geojson
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
if self.pole and self.pole.street and not self.street_id:
|
if self.pole and self.pole.street and not self.street_id:
|
||||||
self.street = self.pole.street
|
self.street = self.pole.street
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -348,13 +348,13 @@ function initMapSources() {
|
||||||
17, 5,
|
17, 5,
|
||||||
22, 6
|
22, 6
|
||||||
],
|
],
|
||||||
'circle-color': '#888888',
|
'circle-color': [
|
||||||
// 'circle-stroke-color': '#333333',
|
'case',
|
||||||
// 'circle-stroke-width': [
|
['==', ['get', 'support_type'], 'trafficlightpole'], '#ef4444',
|
||||||
// 'interpolate', ['linear'], ['zoom'],
|
['==', ['get', 'support_type'], 'lightingpole'], '#f59e0b',
|
||||||
// 8, 0,
|
['==', ['get', 'support_type'], 'facade'], '#8b5cf6',
|
||||||
// 13, 1
|
'#888888'
|
||||||
// ],
|
],
|
||||||
'circle-opacity': 0.8
|
'circle-opacity': 0.8
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -387,7 +387,13 @@ function initMapSources() {
|
||||||
source: 'sign-assets',
|
source: 'sign-assets',
|
||||||
filter: ['==', ['get', 'asset_type'], 'signpole'],
|
filter: ['==', ['get', 'asset_type'], 'signpole'],
|
||||||
layout: {
|
layout: {
|
||||||
'icon-image': 'sign-pole-vertical',
|
'icon-image': [
|
||||||
|
'case',
|
||||||
|
['==', ['get', 'support_type'], 'trafficlightpole'], 'sign-support-trafficlight',
|
||||||
|
['==', ['get', 'support_type'], 'lightingpole'], 'sign-support-lightingpole',
|
||||||
|
['==', ['get', 'support_type'], 'facade'], 'sign-support-facade',
|
||||||
|
'sign-pole-vertical'
|
||||||
|
],
|
||||||
'icon-anchor': 'top',
|
'icon-anchor': 'top',
|
||||||
'icon-offset': ['literal', [0, -64]], // Adjust for pole height
|
'icon-offset': ['literal', [0, -64]], // Adjust for pole height
|
||||||
'icon-allow-overlap': true,
|
'icon-allow-overlap': true,
|
||||||
|
|
@ -494,7 +500,12 @@ function setupMapInteractions() {
|
||||||
|
|
||||||
const feature = e.features[0];
|
const feature = e.features[0];
|
||||||
const coordinates = e.lngLat;
|
const coordinates = e.lngLat;
|
||||||
const description = `<strong>${feature.properties.code || gettext('Poteau')}</strong>`;
|
const supportType = feature.properties.support_type;
|
||||||
|
const supportLabel = feature.properties.support_type_label || (supportType === 'trafficlightpole' ? 'Feu tricolore' : (supportType === 'lightingpole' ? "Poteau d'éclairage" : (supportType === 'facade' ? 'Façade' : 'Poteau')));
|
||||||
|
const supportIcon = supportType === 'trafficlightpole' ? '🚦 ' : '';
|
||||||
|
const supportModelHtml = feature.properties.support_model ? `<div class="small text-muted">${feature.properties.support_model}</div>` : '';
|
||||||
|
const badgeClass = supportType === 'trafficlightpole' ? 'bg-danger text-white' : 'bg-secondary';
|
||||||
|
const description = `<strong>${feature.properties.code || supportLabel}</strong> <span class="badge ${badgeClass}">${supportIcon}${supportLabel}</span>${supportModelHtml}`;
|
||||||
|
|
||||||
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
||||||
});
|
});
|
||||||
|
|
@ -509,7 +520,12 @@ function setupMapInteractions() {
|
||||||
signMap.getCanvas().style.cursor = 'pointer';
|
signMap.getCanvas().style.cursor = 'pointer';
|
||||||
const feature = e.features[0];
|
const feature = e.features[0];
|
||||||
const coordinates = e.lngLat;
|
const coordinates = e.lngLat;
|
||||||
const description = `<strong>${feature.properties.code || gettext('Poteau')}</strong>`;
|
const supportType = feature.properties.support_type;
|
||||||
|
const supportLabel = feature.properties.support_type_label || (supportType === 'trafficlightpole' ? 'Feu tricolore' : (supportType === 'lightingpole' ? "Poteau d'éclairage" : (supportType === 'facade' ? 'Façade' : 'Poteau')));
|
||||||
|
const supportIcon = supportType === 'trafficlightpole' ? '🚦 ' : '';
|
||||||
|
const supportModelHtml = feature.properties.support_model ? `<div class="small text-muted">${feature.properties.support_model}</div>` : '';
|
||||||
|
const badgeClass = supportType === 'trafficlightpole' ? 'bg-danger text-white' : 'bg-secondary';
|
||||||
|
const description = `<strong>${feature.properties.code || supportLabel}</strong> <span class="badge ${badgeClass}">${supportIcon}${supportLabel}</span>${supportModelHtml}`;
|
||||||
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -525,7 +541,8 @@ function setupMapInteractions() {
|
||||||
const feature = e.features[0];
|
const feature = e.features[0];
|
||||||
const coordinates = e.lngLat;
|
const coordinates = e.lngLat;
|
||||||
const panelType = feature.properties.panel_type || '';
|
const panelType = feature.properties.panel_type || '';
|
||||||
const description = `<strong>${panelType || feature.properties.code || gettext('Panneau')}</strong>`;
|
const supportModel = feature.properties.support_model ? `<div class="small text-muted">${feature.properties.support_type_label || 'Support'}: ${feature.properties.support_model}</div>` : (feature.properties.support_type === 'trafficlightpole' ? '<div class="small text-danger">🚦 Sur feu tricolore</div>' : '');
|
||||||
|
const description = `<strong>${panelType || feature.properties.code || gettext('Panneau')}</strong>${supportModel}`;
|
||||||
|
|
||||||
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
||||||
});
|
});
|
||||||
|
|
@ -542,7 +559,8 @@ function setupMapInteractions() {
|
||||||
const feature = e.features[0];
|
const feature = e.features[0];
|
||||||
const coordinates = e.lngLat;
|
const coordinates = e.lngLat;
|
||||||
const panelType = feature.properties.panel_type || '';
|
const panelType = feature.properties.panel_type || '';
|
||||||
const description = `<strong>${panelType || feature.properties.code || gettext('Panneau')}</strong>`;
|
const supportModel = feature.properties.support_model ? `<div class="small text-muted">${feature.properties.support_type_label || 'Support'}: ${feature.properties.support_model}</div>` : (feature.properties.support_type === 'trafficlightpole' ? '<div class="small text-danger">🚦 Sur feu tricolore</div>' : '');
|
||||||
|
const description = `<strong>${panelType || feature.properties.code || gettext('Panneau')}</strong>${supportModel}`;
|
||||||
|
|
||||||
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
signPopup.setLngLat(coordinates).setHTML(description).addTo(signMap);
|
||||||
});
|
});
|
||||||
|
|
@ -897,12 +915,37 @@ function showAssetDetail(feature, assetType) {
|
||||||
if (props.model) {
|
if (props.model) {
|
||||||
detailHtml += `
|
detailHtml += `
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<div class="detail-label">${gettext('Modèle')}</div>
|
<div class="detail-label">${assetType === 'signpanel' ? gettext('Modèle du panneau') : gettext('Modèle')}</div>
|
||||||
<div class="detail-value">${props.model}</div>
|
<div class="detail-value">${props.model}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support type (for both panels and poles)
|
||||||
|
if (props.support_type || props.support_type_label) {
|
||||||
|
const supportTypeLabel = props.support_type_label || props.support_type;
|
||||||
|
const supportIcon = props.support_type === 'trafficlightpole' ? '🚦' : (props.support_type === 'lightingpole' ? '💡' : (props.support_type === 'facade' ? '🏢' : '📍'));
|
||||||
|
const badgeClass = props.support_type === 'trafficlightpole' ? 'bg-danger text-white' : 'bg-secondary';
|
||||||
|
detailHtml += `
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="detail-label">${gettext('Type de support')}</div>
|
||||||
|
<div class="detail-value">
|
||||||
|
<span class="badge ${badgeClass}">${supportIcon} ${supportTypeLabel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support model
|
||||||
|
if (props.support_model) {
|
||||||
|
detailHtml += `
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="detail-label">${gettext('Modèle du support')}</div>
|
||||||
|
<div class="detail-value fw-semibold">${props.support_model}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
if (props.status) {
|
if (props.status) {
|
||||||
detailHtml += `
|
detailHtml += `
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
|
|
@ -959,7 +1002,7 @@ function showAssetDetail(feature, assetType) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Edit Button for poles (after street view buttons)
|
// Edit Button for poles (after street view buttons)
|
||||||
if (assetType === 'signpole' && typeof userPerms !== 'undefined' && userPerms.change_signpole) {
|
if (assetType === 'signpole' && !props.is_standalone_support && typeof userPerms !== 'undefined' && userPerms.change_signpole) {
|
||||||
let lat = props.lat;
|
let lat = props.lat;
|
||||||
let lon = props.lon;
|
let lon = props.lon;
|
||||||
if ((lat == null || lon == null) && feature.geometry && feature.geometry.type === 'Point') {
|
if ((lat == null || lon == null) && feature.geometry && feature.geometry.type === 'Point') {
|
||||||
|
|
@ -977,12 +1020,22 @@ function showAssetDetail(feature, assetType) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.is_standalone_support) {
|
||||||
|
detailHtml += `
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="alert alert-info py-2 px-3 small mb-0">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>${gettext('Support autonome lié au panneau')} <strong>${props.code || ''}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// For poles, list attached panels
|
// For poles, list attached panels
|
||||||
if (assetType === 'signpole') {
|
if (assetType === 'signpole') {
|
||||||
try {
|
try {
|
||||||
const source = signMap.getSource('sign-assets');
|
const source = signMap.getSource('sign-assets');
|
||||||
const features = (source && source._data && source._data.features) ? source._data.features : [];
|
const features = (source && source._data && source._data.features) ? source._data.features : [];
|
||||||
const panels = features.filter(f => f.properties && f.properties.asset_type === 'signpanel' && f.properties.pole_id === feature.id);
|
const panels = features.filter(f => f.properties && f.properties.asset_type === 'signpanel' && (f.properties.pole_id === feature.id || (props.is_standalone_support && f.properties.code === props.code)));
|
||||||
|
|
||||||
// Sort panels by vertical order (if available) or id
|
// Sort panels by vertical order (if available) or id
|
||||||
panels.sort((a, b) => {
|
panels.sort((a, b) => {
|
||||||
|
|
@ -1201,6 +1254,182 @@ function loadRoadSignImages(map) {
|
||||||
data: poleCtx.getImageData(0, 0, poleCanvas.width, poleCanvas.height).data
|
data: poleCtx.getImageData(0, 0, poleCanvas.width, poleCanvas.height).data
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create traffic light support image (mast + 3-light traffic signal box)
|
||||||
|
const tlCanvas = document.createElement('canvas');
|
||||||
|
tlCanvas.width = 24;
|
||||||
|
tlCanvas.height = 64;
|
||||||
|
const tlCtx = tlCanvas.getContext('2d');
|
||||||
|
tlCtx.clearRect(0, 0, tlCanvas.width, tlCanvas.height);
|
||||||
|
|
||||||
|
// Mast (poteau) from y = 28 to y = 64
|
||||||
|
const tlMastWidth = 4;
|
||||||
|
const tlMastX = Math.floor((tlCanvas.width - tlMastWidth) / 2); // 10
|
||||||
|
tlCtx.fillStyle = '#374151';
|
||||||
|
tlCtx.strokeStyle = '#111827';
|
||||||
|
tlCtx.lineWidth = 1;
|
||||||
|
tlCtx.fillRect(tlMastX, 28, tlMastWidth, 36);
|
||||||
|
tlCtx.strokeRect(tlMastX, 28, tlMastWidth, 36);
|
||||||
|
|
||||||
|
// Base plate at bottom
|
||||||
|
tlCtx.fillStyle = '#1f2937';
|
||||||
|
tlCtx.fillRect(tlMastX - 2, 61, tlMastWidth + 4, 3);
|
||||||
|
|
||||||
|
// Traffic light casing (boîtier)
|
||||||
|
const boxW = 14;
|
||||||
|
const boxH = 28;
|
||||||
|
const boxX = Math.floor((tlCanvas.width - boxW) / 2); // 5
|
||||||
|
const boxY = 2;
|
||||||
|
const radius = 3;
|
||||||
|
|
||||||
|
// Outer casing
|
||||||
|
tlCtx.fillStyle = '#111827';
|
||||||
|
tlCtx.strokeStyle = '#e5e7eb';
|
||||||
|
tlCtx.lineWidth = 1.2;
|
||||||
|
tlCtx.beginPath();
|
||||||
|
if (tlCtx.roundRect) {
|
||||||
|
tlCtx.roundRect(boxX, boxY, boxW, boxH, radius);
|
||||||
|
} else {
|
||||||
|
tlCtx.rect(boxX, boxY, boxW, boxH);
|
||||||
|
}
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.stroke();
|
||||||
|
|
||||||
|
// 3 lights: Red, Amber, Green
|
||||||
|
const lightRadius = 3;
|
||||||
|
const lightCenterX = 12;
|
||||||
|
|
||||||
|
// Red light (top)
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX, 7, lightRadius, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ef4444';
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.strokeStyle = '#7f1d1d';
|
||||||
|
tlCtx.lineWidth = 0.8;
|
||||||
|
tlCtx.stroke();
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX - 1, 6, 1, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ffffff';
|
||||||
|
tlCtx.fill();
|
||||||
|
|
||||||
|
// Amber / Yellow light (middle)
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX, 16, lightRadius, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#f59e0b';
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.strokeStyle = '#78350f';
|
||||||
|
tlCtx.lineWidth = 0.8;
|
||||||
|
tlCtx.stroke();
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX - 1, 15, 1, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ffffff';
|
||||||
|
tlCtx.fill();
|
||||||
|
|
||||||
|
// Green light (bottom)
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX, 25, lightRadius, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#10b981';
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.strokeStyle = '#064e3b';
|
||||||
|
tlCtx.lineWidth = 0.8;
|
||||||
|
tlCtx.stroke();
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX - 1, 24, 1, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ffffff';
|
||||||
|
tlCtx.fill();
|
||||||
|
|
||||||
|
map.addImage('sign-support-trafficlight', {
|
||||||
|
width: tlCanvas.width,
|
||||||
|
height: tlCanvas.height,
|
||||||
|
data: tlCtx.getImageData(0, 0, tlCanvas.width, tlCanvas.height).data
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create lighting pole support image (mast with curved arm and lamp head)
|
||||||
|
const lpCanvas = document.createElement('canvas');
|
||||||
|
lpCanvas.width = 24;
|
||||||
|
lpCanvas.height = 64;
|
||||||
|
const lpCtx = lpCanvas.getContext('2d');
|
||||||
|
lpCtx.clearRect(0, 0, lpCanvas.width, lpCanvas.height);
|
||||||
|
|
||||||
|
const lpMastW = 4;
|
||||||
|
const lpMastX = Math.floor((lpCanvas.width - lpMastW) / 2); // 10
|
||||||
|
lpCtx.fillStyle = '#4b5563';
|
||||||
|
lpCtx.strokeStyle = '#1f2937';
|
||||||
|
lpCtx.lineWidth = 1;
|
||||||
|
lpCtx.fillRect(lpMastX, 10, lpMastW, 54);
|
||||||
|
lpCtx.strokeRect(lpMastX, 10, lpMastW, 54);
|
||||||
|
|
||||||
|
// Base plate
|
||||||
|
lpCtx.fillStyle = '#1f2937';
|
||||||
|
lpCtx.fillRect(lpMastX - 2, 61, lpMastW + 4, 3);
|
||||||
|
|
||||||
|
// Curved arm to the left
|
||||||
|
lpCtx.strokeStyle = '#4b5563';
|
||||||
|
lpCtx.lineWidth = 2.5;
|
||||||
|
lpCtx.beginPath();
|
||||||
|
lpCtx.moveTo(lpMastX + 2, 14);
|
||||||
|
lpCtx.quadraticCurveTo(lpMastX + 2, 3, 5, 5);
|
||||||
|
lpCtx.stroke();
|
||||||
|
|
||||||
|
// Luminaire / lamp head
|
||||||
|
lpCtx.fillStyle = '#fbbf24';
|
||||||
|
lpCtx.strokeStyle = '#b45309';
|
||||||
|
lpCtx.lineWidth = 1;
|
||||||
|
lpCtx.beginPath();
|
||||||
|
if (lpCtx.ellipse) {
|
||||||
|
lpCtx.ellipse(5, 5, 4, 2.5, -0.3, 0, 2 * Math.PI);
|
||||||
|
} else {
|
||||||
|
lpCtx.arc(5, 5, 3, 0, 2 * Math.PI);
|
||||||
|
}
|
||||||
|
lpCtx.fill();
|
||||||
|
lpCtx.stroke();
|
||||||
|
|
||||||
|
map.addImage('sign-support-lightingpole', {
|
||||||
|
width: lpCanvas.width,
|
||||||
|
height: lpCanvas.height,
|
||||||
|
data: lpCtx.getImageData(0, 0, lpCanvas.width, lpCanvas.height).data
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create facade support image (wall bracket mount)
|
||||||
|
const fcCanvas = document.createElement('canvas');
|
||||||
|
fcCanvas.width = 24;
|
||||||
|
fcCanvas.height = 64;
|
||||||
|
const fcCtx = fcCanvas.getContext('2d');
|
||||||
|
fcCtx.clearRect(0, 0, fcCanvas.width, fcCanvas.height);
|
||||||
|
|
||||||
|
// Wall texture on left side
|
||||||
|
fcCtx.fillStyle = '#64748b';
|
||||||
|
fcCtx.strokeStyle = '#334155';
|
||||||
|
fcCtx.lineWidth = 1;
|
||||||
|
fcCtx.fillRect(2, 16, 6, 32);
|
||||||
|
fcCtx.strokeRect(2, 16, 6, 32);
|
||||||
|
|
||||||
|
// Mortar lines
|
||||||
|
fcCtx.strokeStyle = '#94a3b8';
|
||||||
|
fcCtx.lineWidth = 0.8;
|
||||||
|
fcCtx.beginPath();
|
||||||
|
fcCtx.moveTo(2, 24); fcCtx.lineTo(8, 24);
|
||||||
|
fcCtx.moveTo(2, 32); fcCtx.lineTo(8, 32);
|
||||||
|
fcCtx.moveTo(2, 40); fcCtx.lineTo(8, 40);
|
||||||
|
fcCtx.stroke();
|
||||||
|
|
||||||
|
// Horizontal bracket arm
|
||||||
|
fcCtx.strokeStyle = '#1e293b';
|
||||||
|
fcCtx.lineWidth = 3;
|
||||||
|
fcCtx.beginPath();
|
||||||
|
fcCtx.moveTo(8, 28);
|
||||||
|
fcCtx.lineTo(16, 28);
|
||||||
|
fcCtx.stroke();
|
||||||
|
|
||||||
|
// Vertical mounting sleeve
|
||||||
|
fcCtx.fillStyle = '#334155';
|
||||||
|
fcCtx.fillRect(14, 18, 4, 20);
|
||||||
|
|
||||||
|
map.addImage('sign-support-facade', {
|
||||||
|
width: fcCanvas.width,
|
||||||
|
height: fcCanvas.height,
|
||||||
|
data: fcCtx.getImageData(0, 0, fcCanvas.width, fcCanvas.height).data
|
||||||
|
});
|
||||||
|
|
||||||
map.addImage('road-sign-images-loaded', {
|
map.addImage('road-sign-images-loaded', {
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|
|
||||||
|
|
@ -924,6 +924,165 @@ function loadRoadSignImages(map) {
|
||||||
data: poleCtx.getImageData(0, 0, poleCanvas.width, poleCanvas.height).data
|
data: poleCtx.getImageData(0, 0, poleCanvas.width, poleCanvas.height).data
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create traffic light support image (mast + 3-light traffic signal box)
|
||||||
|
const tlCanvas = document.createElement('canvas');
|
||||||
|
tlCanvas.width = 24;
|
||||||
|
tlCanvas.height = 64;
|
||||||
|
const tlCtx = tlCanvas.getContext('2d');
|
||||||
|
tlCtx.clearRect(0, 0, tlCanvas.width, tlCanvas.height);
|
||||||
|
|
||||||
|
const tlMastWidth = 4;
|
||||||
|
const tlMastX = Math.floor((tlCanvas.width - tlMastWidth) / 2);
|
||||||
|
tlCtx.fillStyle = '#374151';
|
||||||
|
tlCtx.strokeStyle = '#111827';
|
||||||
|
tlCtx.lineWidth = 1;
|
||||||
|
tlCtx.fillRect(tlMastX, 28, tlMastWidth, 36);
|
||||||
|
tlCtx.strokeRect(tlMastX, 28, tlMastWidth, 36);
|
||||||
|
|
||||||
|
tlCtx.fillStyle = '#1f2937';
|
||||||
|
tlCtx.fillRect(tlMastX - 2, 61, tlMastWidth + 4, 3);
|
||||||
|
|
||||||
|
const boxW = 14;
|
||||||
|
const boxH = 28;
|
||||||
|
const boxX = Math.floor((tlCanvas.width - boxW) / 2);
|
||||||
|
const boxY = 2;
|
||||||
|
const radius = 3;
|
||||||
|
|
||||||
|
tlCtx.fillStyle = '#111827';
|
||||||
|
tlCtx.strokeStyle = '#e5e7eb';
|
||||||
|
tlCtx.lineWidth = 1.2;
|
||||||
|
tlCtx.beginPath();
|
||||||
|
if (tlCtx.roundRect) {
|
||||||
|
tlCtx.roundRect(boxX, boxY, boxW, boxH, radius);
|
||||||
|
} else {
|
||||||
|
tlCtx.rect(boxX, boxY, boxW, boxH);
|
||||||
|
}
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.stroke();
|
||||||
|
|
||||||
|
const lightRadius = 3;
|
||||||
|
const lightCenterX = 12;
|
||||||
|
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX, 7, lightRadius, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ef4444';
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.strokeStyle = '#7f1d1d';
|
||||||
|
tlCtx.lineWidth = 0.8;
|
||||||
|
tlCtx.stroke();
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX - 1, 6, 1, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ffffff';
|
||||||
|
tlCtx.fill();
|
||||||
|
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX, 16, lightRadius, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#f59e0b';
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.strokeStyle = '#78350f';
|
||||||
|
tlCtx.lineWidth = 0.8;
|
||||||
|
tlCtx.stroke();
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX - 1, 15, 1, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ffffff';
|
||||||
|
tlCtx.fill();
|
||||||
|
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX, 25, lightRadius, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#10b981';
|
||||||
|
tlCtx.fill();
|
||||||
|
tlCtx.strokeStyle = '#064e3b';
|
||||||
|
tlCtx.lineWidth = 0.8;
|
||||||
|
tlCtx.stroke();
|
||||||
|
tlCtx.beginPath();
|
||||||
|
tlCtx.arc(lightCenterX - 1, 24, 1, 0, 2 * Math.PI);
|
||||||
|
tlCtx.fillStyle = '#ffffff';
|
||||||
|
tlCtx.fill();
|
||||||
|
|
||||||
|
map.addImage('sign-support-trafficlight', {
|
||||||
|
width: tlCanvas.width,
|
||||||
|
height: tlCanvas.height,
|
||||||
|
data: tlCtx.getImageData(0, 0, tlCanvas.width, tlCanvas.height).data
|
||||||
|
});
|
||||||
|
|
||||||
|
const lpCanvas = document.createElement('canvas');
|
||||||
|
lpCanvas.width = 24;
|
||||||
|
lpCanvas.height = 64;
|
||||||
|
const lpCtx = lpCanvas.getContext('2d');
|
||||||
|
lpCtx.clearRect(0, 0, lpCanvas.width, lpCanvas.height);
|
||||||
|
|
||||||
|
const lpMastW = 4;
|
||||||
|
const lpMastX = Math.floor((lpCanvas.width - lpMastW) / 2);
|
||||||
|
lpCtx.fillStyle = '#4b5563';
|
||||||
|
lpCtx.strokeStyle = '#1f2937';
|
||||||
|
lpCtx.lineWidth = 1;
|
||||||
|
lpCtx.fillRect(lpMastX, 10, lpMastW, 54);
|
||||||
|
lpCtx.strokeRect(lpMastX, 10, lpMastW, 54);
|
||||||
|
|
||||||
|
lpCtx.fillStyle = '#1f2937';
|
||||||
|
lpCtx.fillRect(lpMastX - 2, 61, lpMastW + 4, 3);
|
||||||
|
|
||||||
|
lpCtx.strokeStyle = '#4b5563';
|
||||||
|
lpCtx.lineWidth = 2.5;
|
||||||
|
lpCtx.beginPath();
|
||||||
|
lpCtx.moveTo(lpMastX + 2, 14);
|
||||||
|
lpCtx.quadraticCurveTo(lpMastX + 2, 3, 5, 5);
|
||||||
|
lpCtx.stroke();
|
||||||
|
|
||||||
|
lpCtx.fillStyle = '#fbbf24';
|
||||||
|
lpCtx.strokeStyle = '#b45309';
|
||||||
|
lpCtx.lineWidth = 1;
|
||||||
|
lpCtx.beginPath();
|
||||||
|
if (lpCtx.ellipse) {
|
||||||
|
lpCtx.ellipse(5, 5, 4, 2.5, -0.3, 0, 2 * Math.PI);
|
||||||
|
} else {
|
||||||
|
lpCtx.arc(5, 5, 3, 0, 2 * Math.PI);
|
||||||
|
}
|
||||||
|
lpCtx.fill();
|
||||||
|
lpCtx.stroke();
|
||||||
|
|
||||||
|
map.addImage('sign-support-lightingpole', {
|
||||||
|
width: lpCanvas.width,
|
||||||
|
height: lpCanvas.height,
|
||||||
|
data: lpCtx.getImageData(0, 0, lpCanvas.width, lpCanvas.height).data
|
||||||
|
});
|
||||||
|
|
||||||
|
const fcCanvas = document.createElement('canvas');
|
||||||
|
fcCanvas.width = 24;
|
||||||
|
fcCanvas.height = 64;
|
||||||
|
const fcCtx = fcCanvas.getContext('2d');
|
||||||
|
fcCtx.clearRect(0, 0, fcCanvas.width, fcCanvas.height);
|
||||||
|
|
||||||
|
fcCtx.fillStyle = '#64748b';
|
||||||
|
fcCtx.strokeStyle = '#334155';
|
||||||
|
fcCtx.lineWidth = 1;
|
||||||
|
fcCtx.fillRect(2, 16, 6, 32);
|
||||||
|
fcCtx.strokeRect(2, 16, 6, 32);
|
||||||
|
|
||||||
|
fcCtx.strokeStyle = '#94a3b8';
|
||||||
|
fcCtx.lineWidth = 0.8;
|
||||||
|
fcCtx.beginPath();
|
||||||
|
fcCtx.moveTo(2, 24); fcCtx.lineTo(8, 24);
|
||||||
|
fcCtx.moveTo(2, 32); fcCtx.lineTo(8, 32);
|
||||||
|
fcCtx.moveTo(2, 40); fcCtx.lineTo(8, 40);
|
||||||
|
fcCtx.stroke();
|
||||||
|
|
||||||
|
fcCtx.strokeStyle = '#1e293b';
|
||||||
|
fcCtx.lineWidth = 3;
|
||||||
|
fcCtx.beginPath();
|
||||||
|
fcCtx.moveTo(8, 28);
|
||||||
|
fcCtx.lineTo(16, 28);
|
||||||
|
fcCtx.stroke();
|
||||||
|
|
||||||
|
fcCtx.fillStyle = '#334155';
|
||||||
|
fcCtx.fillRect(14, 18, 4, 20);
|
||||||
|
|
||||||
|
map.addImage('sign-support-facade', {
|
||||||
|
width: fcCanvas.width,
|
||||||
|
height: fcCanvas.height,
|
||||||
|
data: fcCtx.getImageData(0, 0, fcCanvas.width, fcCanvas.height).data
|
||||||
|
});
|
||||||
|
|
||||||
map.addImage('road-sign-images-loaded', {
|
map.addImage('road-sign-images-loaded', {
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|
@ -1217,7 +1376,13 @@ function initMapSources() {
|
||||||
source: 'regulation-panels',
|
source: 'regulation-panels',
|
||||||
filter: ['==', ['get', 'asset_type'], 'signpole'],
|
filter: ['==', ['get', 'asset_type'], 'signpole'],
|
||||||
layout: {
|
layout: {
|
||||||
'icon-image': 'sign-pole-vertical',
|
'icon-image': [
|
||||||
|
'case',
|
||||||
|
['==', ['get', 'support_type'], 'trafficlightpole'], 'sign-support-trafficlight',
|
||||||
|
['==', ['get', 'support_type'], 'lightingpole'], 'sign-support-lightingpole',
|
||||||
|
['==', ['get', 'support_type'], 'facade'], 'sign-support-facade',
|
||||||
|
'sign-pole-vertical'
|
||||||
|
],
|
||||||
'icon-anchor': 'top',
|
'icon-anchor': 'top',
|
||||||
'icon-offset': ['literal', [0, -5]],
|
'icon-offset': ['literal', [0, -5]],
|
||||||
'icon-allow-overlap': true,
|
'icon-allow-overlap': true,
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,32 @@
|
||||||
<strong>Code:</strong> {{ asset.code|default:"-" }}<br>
|
<strong>Code:</strong> {{ asset.code|default:"-" }}<br>
|
||||||
<strong>Organisation:</strong> {{ asset.organization|default:"-" }}<br>
|
<strong>Organisation:</strong> {{ asset.organization|default:"-" }}<br>
|
||||||
{% if asset_model == 'signpole' %}
|
{% if asset_model == 'signpole' %}
|
||||||
<strong>Modèle:</strong> {{ asset.model|default:"-" }}<br>
|
<strong>{% translate "Type de support:" %}</strong>
|
||||||
<strong>Hauteur:</strong> {{ asset.height|default:"-" }} m<br>
|
{% if asset.support_type == 'trafficlightpole' %}
|
||||||
<strong>Couleur:</strong> {{ asset.color|default:"-" }}<br>
|
<span class="badge bg-danger-subtle text-danger border border-danger">🚦 {{ asset.support_type_display }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark border">{{ asset.support_type_display|default:"Poteau" }}</span>
|
||||||
|
{% endif %}<br>
|
||||||
|
<strong>{% translate "Modèle:" %}</strong> {{ asset.model|default:"-" }}<br>
|
||||||
|
<strong>{% translate "Hauteur:" %}</strong> {{ asset.height|default:"-" }} m<br>
|
||||||
|
<strong>{% translate "Couleur:" %}</strong> {{ asset.color|default:"-" }}<br>
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong>Type:</strong> {{ asset.signpanel_type.code }} - {{ asset.signpanel_type.get_name }}<br>
|
<strong>{% translate "Type de panneau:" %}</strong> {{ asset.signpanel_type.code }} - {{ asset.signpanel_type.get_name }}<br>
|
||||||
<strong>Texte:</strong> {{ asset.signpanel_text|default:"-" }}<br>
|
<strong>{% translate "Support:" %}</strong>
|
||||||
|
{% if asset.effective_support_type == 'trafficlightpole' %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger">🚦 {{ asset.effective_support_type_display }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark border">{{ asset.effective_support_type_display|default:"Poteau" }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if asset.pole %}(<a href="{% url 'sign:sign_assets_detail' 'signpole' asset.pole.id %}">{{ asset.pole.code }}</a>){% endif %}
|
||||||
|
<br>
|
||||||
|
{% if asset.support_model_display %}
|
||||||
|
<strong>{% translate "Modèle du support:" %}</strong> <span class="fw-semibold">{{ asset.support_model_display }}</span><br>
|
||||||
|
{% endif %}
|
||||||
|
{% if asset.model %}
|
||||||
|
<strong>{% translate "Modèle du panneau:" %}</strong> {{ asset.model }}<br>
|
||||||
|
{% endif %}
|
||||||
|
<strong>{% translate "Texte:" %}</strong> {{ asset.signpanel_text|default:"-" }}<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,12 @@
|
||||||
{{ form.support_type }}
|
{{ form.support_type }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.support_model.id_for_label }}" class="form-label">{{ form.support_model.label }}</label>
|
||||||
|
{{ form.support_model }}
|
||||||
|
<div class="form-text text-muted small">{% translate "Précisez le modèle du support (ex: Poteau Acier 3m, Feu tricolore carrefour, Candélabre...)" %}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.orientation.id_for_label }}" class="form-label">{{ form.orientation.label }}</label>
|
<label for="{{ form.orientation.id_for_label }}" class="form-label">{{ form.orientation.label }}</label>
|
||||||
{{ form.orientation }}
|
{{ form.orientation }}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.support_type.id_for_label }}" class="form-label">{{ form.support_type.label }}</label>
|
||||||
|
{{ form.support_type }}
|
||||||
|
{% if form.support_type.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ form.support_type.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.organization.id_for_label }}" class="form-label">{{ form.organization.label }}</label>
|
<label for="{{ form.organization.id_for_label }}" class="form-label">{{ form.organization.label }}</label>
|
||||||
{{ form.organization }}
|
{{ form.organization }}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.support_type.id_for_label }}" class="form-label">{{ form.support_type.label }}</label>
|
||||||
|
{{ form.support_type }}
|
||||||
|
{% if form.support_type.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ form.support_type.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.organization.id_for_label }}" class="form-label">{{ form.organization.label }}</label>
|
<label for="{{ form.organization.id_for_label }}" class="form-label">{{ form.organization.label }}</label>
|
||||||
{{ form.organization }}
|
{{ form.organization }}
|
||||||
|
|
|
||||||
|
|
@ -1000,3 +1000,205 @@ class SignStreetAssignmentTests(TestCase):
|
||||||
self.assertEqual(panel.vertical_order, 1)
|
self.assertEqual(panel.vertical_order, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class SupportRepresentationAndModelTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from common.models import Municipality, AdrStreet, UserConfig, AppView
|
||||||
|
from sign.models import SignPanelType, SignPoleModel, SignPanelModel, SignPole, SignPanel, Organization
|
||||||
|
from assets.models import TrafficLightIntersection, TrafficLightPoleModel, TrafficLightPole
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
self.user = User.objects.create_user(username="support_user", password="password", is_superuser=True)
|
||||||
|
sign_view, _ = AppView.objects.get_or_create(
|
||||||
|
code="sign",
|
||||||
|
defaults={
|
||||||
|
"name_fr": "Signalisation",
|
||||||
|
"name_nl": "Signalisatie",
|
||||||
|
"url_name": "sign:index",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.config = UserConfig.objects.create(user=self.user, esign_can_edit=True)
|
||||||
|
self.config.accessible_views.add(sign_view)
|
||||||
|
|
||||||
|
self.muni = Municipality.objects.create(fid="1080", name_fr="Molenbeek", name_nl="Molenbeek")
|
||||||
|
self.adr_street = AdrStreet.objects.create(
|
||||||
|
fid="STR-100",
|
||||||
|
name_fr="Rue de Test",
|
||||||
|
name_nl="Teststraat",
|
||||||
|
municipality=self.muni,
|
||||||
|
)
|
||||||
|
self.org = Organization.objects.create(
|
||||||
|
code="TEST_ORG",
|
||||||
|
name_fr="Test Organisation",
|
||||||
|
name_nl="Test Organisatie",
|
||||||
|
organization_type="region",
|
||||||
|
)
|
||||||
|
self.pole_model = SignPoleModel.objects.create(
|
||||||
|
code="POL-MOD-1",
|
||||||
|
name_fr="Modèle Mât 4m",
|
||||||
|
)
|
||||||
|
self.panel_model = SignPanelModel.objects.create(
|
||||||
|
code="PAN-MOD-1",
|
||||||
|
name_fr="Modèle Type B",
|
||||||
|
)
|
||||||
|
self.panel_type = SignPanelType.objects.create(
|
||||||
|
code="B1",
|
||||||
|
name_fr="Céder le passage",
|
||||||
|
name_nl="Voorrang verlenen",
|
||||||
|
default_width=70.0,
|
||||||
|
default_height=70.0,
|
||||||
|
)
|
||||||
|
self.tl_intersection = TrafficLightIntersection.objects.create(
|
||||||
|
code="TL-INT-01",
|
||||||
|
name_fr="Carrefour Test",
|
||||||
|
)
|
||||||
|
self.tl_pole_model = TrafficLightPoleModel.objects.create(
|
||||||
|
code="TL-PM-01",
|
||||||
|
name_fr="Poteau Tricolore Acier 6m",
|
||||||
|
)
|
||||||
|
self.tl_pole = TrafficLightPole.objects.create(
|
||||||
|
code="TL-P-01",
|
||||||
|
intersection=self.tl_intersection,
|
||||||
|
model=self.tl_pole_model,
|
||||||
|
geom=Point(145000, 172000, srid=3812),
|
||||||
|
lon=4.32,
|
||||||
|
lat=50.85,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_signpole_support_type(self):
|
||||||
|
"""Test that SignPole supports support_type choices and defaults."""
|
||||||
|
from sign.models import SignPole
|
||||||
|
pole = SignPole.objects.create(
|
||||||
|
code="P-TEST-01",
|
||||||
|
street=self.adr_street,
|
||||||
|
organization=self.org,
|
||||||
|
support_type="trafficlightpole",
|
||||||
|
model=self.pole_model,
|
||||||
|
)
|
||||||
|
self.assertEqual(pole.support_type, "trafficlightpole")
|
||||||
|
self.assertEqual(pole.support_type_display, "Poteau de signalisation lumineuse")
|
||||||
|
|
||||||
|
def test_signpanel_effective_support_and_model(self):
|
||||||
|
"""Test effective_support_type and support_model_display on SignPanel."""
|
||||||
|
from sign.models import SignPole, SignPanel
|
||||||
|
|
||||||
|
# 1. Panel attached to pole with support_type
|
||||||
|
pole = SignPole.objects.create(
|
||||||
|
code="P-TEST-02",
|
||||||
|
street=self.adr_street,
|
||||||
|
organization=self.org,
|
||||||
|
support_type="lightingpole",
|
||||||
|
model=self.pole_model,
|
||||||
|
)
|
||||||
|
panel1 = SignPanel.objects.create(
|
||||||
|
code="PANEL-01",
|
||||||
|
pole=pole,
|
||||||
|
model=self.panel_model,
|
||||||
|
signpanel_type=self.panel_type,
|
||||||
|
organization=self.org,
|
||||||
|
)
|
||||||
|
self.assertEqual(panel1.effective_support_type, "lightingpole")
|
||||||
|
self.assertEqual(panel1.effective_support_type_display, "Poteau d'éclairage")
|
||||||
|
self.assertEqual(panel1.support_model_display, "Modèle Mât 4m")
|
||||||
|
|
||||||
|
# 2. Panel attached directly to trafficlight_pole
|
||||||
|
panel2 = SignPanel.objects.create(
|
||||||
|
code="PANEL-02",
|
||||||
|
trafficlight_pole=self.tl_pole,
|
||||||
|
model=self.panel_model,
|
||||||
|
signpanel_type=self.panel_type,
|
||||||
|
organization=self.org,
|
||||||
|
)
|
||||||
|
self.assertEqual(panel2.effective_support_type, "trafficlightpole")
|
||||||
|
self.assertEqual(panel2.effective_support_type_display, "Poteau de signalisation lumineuse")
|
||||||
|
self.assertEqual(panel2.support_model_display, "Poteau Tricolore Acier 6m")
|
||||||
|
|
||||||
|
# 3. Standalone panel with support_type and custom support_model
|
||||||
|
panel3 = SignPanel.objects.create(
|
||||||
|
code="PANEL-03",
|
||||||
|
support_type="trafficlightpole",
|
||||||
|
support_model="Feu Swarco Carrefour",
|
||||||
|
model=self.panel_model,
|
||||||
|
signpanel_type=self.panel_type,
|
||||||
|
organization=self.org,
|
||||||
|
)
|
||||||
|
self.assertEqual(panel3.effective_support_type, "trafficlightpole")
|
||||||
|
self.assertEqual(panel3.effective_support_type_display, "Poteau de signalisation lumineuse")
|
||||||
|
self.assertEqual(panel3.support_model_display, "Feu Swarco Carrefour")
|
||||||
|
|
||||||
|
def test_sign_assets_geojson_includes_support_representation(self):
|
||||||
|
"""Test that sign_assets_geojson includes support_type, support_model and standalone support features."""
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from sign.models import SignPanel
|
||||||
|
import json
|
||||||
|
|
||||||
|
pt = Point(145000, 172000, srid=3812)
|
||||||
|
panel = SignPanel.objects.create(
|
||||||
|
code="PANEL-TL-01",
|
||||||
|
street=self.adr_street,
|
||||||
|
support_type="trafficlightpole",
|
||||||
|
support_model="Feu Siemens 3 Feux",
|
||||||
|
model=self.panel_model,
|
||||||
|
signpanel_type=self.panel_type,
|
||||||
|
organization=self.org,
|
||||||
|
geom=pt,
|
||||||
|
lon=4.32,
|
||||||
|
lat=50.85,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('sign:sign_assets_geojson'),
|
||||||
|
data=json.dumps({'organization': [self.org.id]}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
features = data.get('features', [])
|
||||||
|
self.assertTrue(len(features) >= 2)
|
||||||
|
|
||||||
|
panel_feat = next((f for f in features if f['properties'].get('asset_type') == 'signpanel'), None)
|
||||||
|
self.assertIsNotNone(panel_feat)
|
||||||
|
self.assertEqual(panel_feat['properties']['support_type'], 'trafficlightpole')
|
||||||
|
self.assertEqual(panel_feat['properties']['support_model'], 'Feu Siemens 3 Feux')
|
||||||
|
|
||||||
|
support_feat = next((f for f in features if f['properties'].get('asset_type') == 'signpole'), None)
|
||||||
|
self.assertIsNotNone(support_feat)
|
||||||
|
self.assertEqual(support_feat['properties']['support_type'], 'trafficlightpole')
|
||||||
|
self.assertEqual(support_feat['properties']['support_model'], 'Feu Siemens 3 Feux')
|
||||||
|
self.assertTrue(support_feat['properties'].get('is_standalone_support'))
|
||||||
|
|
||||||
|
def test_sign_poles_overview_geojson_includes_standalone_trafficlight(self):
|
||||||
|
"""Test sign_poles_overview_geojson includes standalone panel supports."""
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from sign.models import SignPanel
|
||||||
|
import json
|
||||||
|
|
||||||
|
pt = Point(145000, 172000, srid=3812)
|
||||||
|
SignPanel.objects.create(
|
||||||
|
code="PANEL-TL-OV",
|
||||||
|
street=self.adr_street,
|
||||||
|
support_type="trafficlightpole",
|
||||||
|
support_model="Feu Carrefour 1",
|
||||||
|
organization=self.org,
|
||||||
|
geom=pt,
|
||||||
|
lon=4.32,
|
||||||
|
lat=50.85,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('sign:sign_poles_overview_geojson'),
|
||||||
|
data=json.dumps({'organization': [self.org.id]}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
features = data.get('features', [])
|
||||||
|
tl_support = next((f for f in features if f['properties'].get('support_type') == 'trafficlightpole'), None)
|
||||||
|
self.assertIsNotNone(tl_support)
|
||||||
|
self.assertEqual(tl_support['properties']['support_model'], 'Feu Carrefour 1')
|
||||||
|
self.assertTrue(tl_support['properties'].get('is_standalone_support'))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ from .models import (
|
||||||
RegulationMeasurePanel,
|
RegulationMeasurePanel,
|
||||||
SignPanelTypeArticle,
|
SignPanelTypeArticle,
|
||||||
RegulationArticleVoirie,
|
RegulationArticleVoirie,
|
||||||
|
SUPPORT_TYPE_CHOICES,
|
||||||
)
|
)
|
||||||
from .permissions import user_can_manage_regulations, user_can_edit_regulation, user_can_submit_regulation, user_can_edit_esign_assets, user_can_view_regulation
|
from .permissions import user_can_manage_regulations, user_can_edit_regulation, user_can_submit_regulation, user_can_edit_esign_assets, user_can_view_regulation
|
||||||
from .forms import SignPoleForm, SignPanelForm
|
from .forms import SignPoleForm, SignPanelForm
|
||||||
|
|
@ -532,12 +533,16 @@ def sign_poles_overview_geojson(request):
|
||||||
|
|
||||||
# Use values() to fetch only the needed columns in a single query,
|
# Use values() to fetch only the needed columns in a single query,
|
||||||
# and build the detail URL without get_absolute_url() to avoid instantiation.
|
# and build the detail URL without get_absolute_url() to avoid instantiation.
|
||||||
poles = qs.values('id', 'code', 'lon', 'lat')
|
poles = qs.values('id', 'code', 'lon', 'lat', 'support_type', 'model__name_fr')
|
||||||
|
|
||||||
features = []
|
features = []
|
||||||
|
seen_coords = set()
|
||||||
for pole in poles.iterator(chunk_size=2000):
|
for pole in poles.iterator(chunk_size=2000):
|
||||||
lon, lat = pole['lon'], pole['lat']
|
lon, lat = pole['lon'], pole['lat']
|
||||||
|
if lon is not None and lat is not None:
|
||||||
|
seen_coords.add((round(float(lon), 6), round(float(lat), 6)))
|
||||||
geometry = {"type": "Point", "coordinates": [lon, lat]} if lon is not None and lat is not None else None
|
geometry = {"type": "Point", "coordinates": [lon, lat]} if lon is not None and lat is not None else None
|
||||||
|
support_t = pole.get('support_type') or 'pole'
|
||||||
features.append({
|
features.append({
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"id": pole['id'],
|
"id": pole['id'],
|
||||||
|
|
@ -545,12 +550,51 @@ def sign_poles_overview_geojson(request):
|
||||||
"properties": {
|
"properties": {
|
||||||
"asset_type": "signpole",
|
"asset_type": "signpole",
|
||||||
"code": pole['code'],
|
"code": pole['code'],
|
||||||
|
"support_type": support_t,
|
||||||
|
"support_type_label": dict(SUPPORT_TYPE_CHOICES).get(support_t, support_t or "Poteau"),
|
||||||
|
"support_model": pole.get('model__name_fr') or None,
|
||||||
"lat": lat,
|
"lat": lat,
|
||||||
"lon": lon,
|
"lon": lon,
|
||||||
"detail_url": f"/sign/assets/signpole/{pole['id']}/",
|
"detail_url": f"/sign/assets/signpole/{pole['id']}/",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Include standalone panel supports (panels without pole, e.g. on traffic lights)
|
||||||
|
panel_supports = SignPanel.objects.filter(
|
||||||
|
Q(pole__isnull=True) & (Q(support_type__isnull=False) | Q(trafficlight_pole__isnull=False))
|
||||||
|
)
|
||||||
|
if organizations:
|
||||||
|
panel_supports = panel_supports.filter(organization_id__in=organizations)
|
||||||
|
if municipalities:
|
||||||
|
panel_supports = panel_supports.filter(street__municipality_id__in=municipalities)
|
||||||
|
if filter_geom_raw and filter_geom:
|
||||||
|
panel_supports = panel_supports.filter(geom__intersects=filter_geom)
|
||||||
|
|
||||||
|
for p in panel_supports.values('id', 'code', 'lon', 'lat', 'support_type', 'trafficlight_pole_id', 'support_model'):
|
||||||
|
lon, lat = p['lon'], p['lat']
|
||||||
|
if lon is not None and lat is not None:
|
||||||
|
coord_key = (round(float(lon), 6), round(float(lat), 6))
|
||||||
|
if coord_key not in seen_coords:
|
||||||
|
seen_coords.add(coord_key)
|
||||||
|
st = p['support_type'] or ('trafficlightpole' if p.get('trafficlight_pole_id') else 'pole')
|
||||||
|
st_label = dict(SUPPORT_TYPE_CHOICES).get(st, st or "Poteau")
|
||||||
|
features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": f"sp-{p['id']}",
|
||||||
|
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
||||||
|
"properties": {
|
||||||
|
"asset_type": "signpole",
|
||||||
|
"is_standalone_support": True,
|
||||||
|
"code": p['code'],
|
||||||
|
"support_type": st,
|
||||||
|
"support_type_label": st_label,
|
||||||
|
"support_model": p.get('support_model') or None,
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"detail_url": f"/sign/assets/signpanel/{p['id']}/",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return JsonResponse({"type": "FeatureCollection", "features": features})
|
return JsonResponse({"type": "FeatureCollection", "features": features})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -665,9 +709,13 @@ def sign_assets_geojson(request):
|
||||||
else:
|
else:
|
||||||
pole_orientation_offset = [0, pole_offset]
|
pole_orientation_offset = [0, pole_offset]
|
||||||
|
|
||||||
|
support_t = getattr(obj, "support_type", "pole") or "pole"
|
||||||
feature["properties"].update({
|
feature["properties"].update({
|
||||||
"pole_orientation_offset": pole_orientation_offset,
|
"pole_orientation_offset": pole_orientation_offset,
|
||||||
"show_orientation_arrow": show_orientation_arrow,
|
"show_orientation_arrow": show_orientation_arrow,
|
||||||
|
"support_type": support_t,
|
||||||
|
"support_type_label": getattr(obj, "support_type_display", dict(SUPPORT_TYPE_CHOICES).get(support_t, "Poteau")),
|
||||||
|
"support_model": obj.model.get_name() if obj.model else None,
|
||||||
})
|
})
|
||||||
if asset_type == "signpanel":
|
if asset_type == "signpanel":
|
||||||
panel_meta = panel_render_meta.get(obj.id, {})
|
panel_meta = panel_render_meta.get(obj.id, {})
|
||||||
|
|
@ -703,6 +751,9 @@ def sign_assets_geojson(request):
|
||||||
aspect_ratio = panel_meta.get("aspect_ratio", 1.0)
|
aspect_ratio = panel_meta.get("aspect_ratio", 1.0)
|
||||||
feature["properties"].update({
|
feature["properties"].update({
|
||||||
"pole_id": getattr(obj, "pole_id", None),
|
"pole_id": getattr(obj, "pole_id", None),
|
||||||
|
"support_type": obj.effective_support_type,
|
||||||
|
"support_type_label": obj.effective_support_type_display,
|
||||||
|
"support_model": obj.support_model_display,
|
||||||
"panel_text_can_render": can_render_text,
|
"panel_text_can_render": can_render_text,
|
||||||
"panel_text_image_id": sprite_id,
|
"panel_text_image_id": sprite_id,
|
||||||
"panel_hide_icon": can_render_text,
|
"panel_hide_icon": can_render_text,
|
||||||
|
|
@ -725,7 +776,10 @@ def sign_assets_geojson(request):
|
||||||
return feature_count, False
|
return feature_count, False
|
||||||
|
|
||||||
signpole_qs = SignPole.objects.select_related('street', 'category', 'model').all()
|
signpole_qs = SignPole.objects.select_related('street', 'category', 'model').all()
|
||||||
signpanel_qs = SignPanel.objects.select_related('street', 'category', 'model', 'signpanel_type').all()
|
signpanel_qs = SignPanel.objects.select_related(
|
||||||
|
'street', 'category', 'model', 'signpanel_type', 'pole', 'pole__model',
|
||||||
|
'trafficlight_pole', 'trafficlight_pole__model'
|
||||||
|
).all()
|
||||||
|
|
||||||
if municipalities:
|
if municipalities:
|
||||||
signpole_qs = signpole_qs.filter(street__municipality_id__in=municipalities)
|
signpole_qs = signpole_qs.filter(street__municipality_id__in=municipalities)
|
||||||
|
|
@ -819,6 +873,44 @@ def sign_assets_geojson(request):
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
"error": f"{_('Trop d\'objets à afficher')} (> {MAX_ASSET_FEATURES}). {_('Limitez votre sélection.')}"
|
"error": f"{_('Trop d\'objets à afficher')} (> {MAX_ASSET_FEATURES}). {_('Limitez votre sélection.')}"
|
||||||
}, status=413)
|
}, status=413)
|
||||||
|
|
||||||
|
# Add standalone supports for panels without a SignPole (e.g. traffic lights, facades, lighting poles)
|
||||||
|
seen_standalone_coords = set()
|
||||||
|
for f in features:
|
||||||
|
if f.get("properties", {}).get("asset_type") == "signpole":
|
||||||
|
geom = f.get("geometry")
|
||||||
|
if geom and geom.get("type") == "Point":
|
||||||
|
coords = geom.get("coordinates")
|
||||||
|
if coords and len(coords) >= 2:
|
||||||
|
seen_standalone_coords.add((round(float(coords[0]), 6), round(float(coords[1]), 6)))
|
||||||
|
|
||||||
|
for panel in signpanel_list:
|
||||||
|
if panel.pole_id is None and (panel.support_type or panel.trafficlight_pole_id):
|
||||||
|
if panel.lon is not None and panel.lat is not None:
|
||||||
|
coord_key = (round(float(panel.lon), 6), round(float(panel.lat), 6))
|
||||||
|
if coord_key not in seen_standalone_coords:
|
||||||
|
seen_standalone_coords.add(coord_key)
|
||||||
|
features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": f"sp-{panel.id}",
|
||||||
|
"geometry": json.loads(panel.geojson) if panel.geojson else {"type": "Point", "coordinates": [panel.lon, panel.lat]},
|
||||||
|
"properties": {
|
||||||
|
"code": panel.code,
|
||||||
|
"name": panel.code,
|
||||||
|
"category": panel.category.get_name() if panel.category else None,
|
||||||
|
"model": None,
|
||||||
|
"support_type": panel.effective_support_type,
|
||||||
|
"support_type_label": panel.effective_support_type_display,
|
||||||
|
"support_model": panel.support_model_display,
|
||||||
|
"status": panel.status,
|
||||||
|
"lon": panel.lon,
|
||||||
|
"lat": panel.lat,
|
||||||
|
"asset_type": "signpole",
|
||||||
|
"is_standalone_support": True,
|
||||||
|
"detail_url": panel.get_absolute_url() if hasattr(panel, 'get_absolute_url') else None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
features = sorted(features, key=lambda f: f["properties"]["code"])
|
features = sorted(features, key=lambda f: f["properties"]["code"])
|
||||||
geojson_data = {"type": "FeatureCollection", "features": features}
|
geojson_data = {"type": "FeatureCollection", "features": features}
|
||||||
return JsonResponse(geojson_data, safe=False)
|
return JsonResponse(geojson_data, safe=False)
|
||||||
|
|
@ -1289,7 +1381,10 @@ def regulation_panels_geojson(request, regulation_id: int):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Build queryset
|
# Build queryset
|
||||||
panels_qs = SignPanel.objects.select_related('street', 'signpanel_type', 'pole', 'category', 'model')
|
panels_qs = SignPanel.objects.select_related(
|
||||||
|
'street', 'signpanel_type', 'pole', 'pole__model', 'category', 'model',
|
||||||
|
'trafficlight_pole', 'trafficlight_pole__model'
|
||||||
|
)
|
||||||
|
|
||||||
if filter_geom:
|
if filter_geom:
|
||||||
panels_qs = panels_qs.filter(geom__intersects=filter_geom)
|
panels_qs = panels_qs.filter(geom__intersects=filter_geom)
|
||||||
|
|
@ -1424,6 +1519,9 @@ def regulation_panels_geojson(request, regulation_id: int):
|
||||||
"street": panel.street.get_name() if panel.street else "-",
|
"street": panel.street.get_name() if panel.street else "-",
|
||||||
"status": panel.status,
|
"status": panel.status,
|
||||||
"asset_type": "signpanel",
|
"asset_type": "signpanel",
|
||||||
|
"support_type": panel.effective_support_type,
|
||||||
|
"support_type_label": panel.effective_support_type_display,
|
||||||
|
"support_model": panel.support_model_display,
|
||||||
"panel_icon_image": panel_icon_image,
|
"panel_icon_image": panel_icon_image,
|
||||||
"panel_icon_image_scaled": panel_icon_image_scaled,
|
"panel_icon_image_scaled": panel_icon_image_scaled,
|
||||||
"is_associated": is_associated,
|
"is_associated": is_associated,
|
||||||
|
|
@ -1446,7 +1544,54 @@ def regulation_panels_geojson(request, regulation_id: int):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
features = [obj_to_feature(panel) for panel in panels_list]
|
# Also include support features (poles and standalone supports) so regulation map can render them
|
||||||
|
pole_features = []
|
||||||
|
seen_pole_ids = set()
|
||||||
|
seen_support_coords = set()
|
||||||
|
for panel in panels_list:
|
||||||
|
if panel.pole_id and panel.pole_id not in seen_pole_ids:
|
||||||
|
seen_pole_ids.add(panel.pole_id)
|
||||||
|
pole = panel.pole
|
||||||
|
if pole and pole.lon is not None and pole.lat is not None:
|
||||||
|
pole_geom = json.loads(pole.geojson) if pole.geojson else {"type": "Point", "coordinates": [pole.lon, pole.lat]}
|
||||||
|
support_t = getattr(pole, 'support_type', 'pole') or 'pole'
|
||||||
|
support_m = pole.model.get_name() if pole.model else None
|
||||||
|
pole_features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": f"pole-{pole.id}",
|
||||||
|
"geometry": pole_geom,
|
||||||
|
"properties": {
|
||||||
|
"asset_type": "signpole",
|
||||||
|
"code": pole.code,
|
||||||
|
"support_type": support_t,
|
||||||
|
"support_type_label": getattr(pole, 'support_type_display', dict(SUPPORT_TYPE_CHOICES).get(support_t, 'Poteau')),
|
||||||
|
"support_model": support_m,
|
||||||
|
"lon": pole.lon,
|
||||||
|
"lat": pole.lat,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
elif not panel.pole_id and (panel.support_type or panel.trafficlight_pole_id):
|
||||||
|
if panel.lon is not None and panel.lat is not None:
|
||||||
|
coord_key = (round(float(panel.lon), 6), round(float(panel.lat), 6))
|
||||||
|
if coord_key not in seen_support_coords:
|
||||||
|
seen_support_coords.add(coord_key)
|
||||||
|
pole_features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": f"support-panel-{panel.id}",
|
||||||
|
"geometry": json.loads(panel.geojson) if panel.geojson else {"type": "Point", "coordinates": [panel.lon, panel.lat]},
|
||||||
|
"properties": {
|
||||||
|
"asset_type": "signpole",
|
||||||
|
"is_standalone_support": True,
|
||||||
|
"code": panel.code,
|
||||||
|
"support_type": panel.effective_support_type,
|
||||||
|
"support_type_label": panel.effective_support_type_display,
|
||||||
|
"support_model": panel.support_model_display,
|
||||||
|
"lon": panel.lon,
|
||||||
|
"lat": panel.lat,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
features = [obj_to_feature(panel) for panel in panels_list] + pole_features
|
||||||
|
|
||||||
geojson_data = {
|
geojson_data = {
|
||||||
"type": "FeatureCollection",
|
"type": "FeatureCollection",
|
||||||
|
|
@ -2323,6 +2468,18 @@ def sign_panel_create(request):
|
||||||
panel.geojson = pole.geojson
|
panel.geojson = pole.geojson
|
||||||
except SignPole.DoesNotExist:
|
except SignPole.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
elif panel.support_type == 'trafficlightpole' and lat and lon:
|
||||||
|
from assets.models import TrafficLightPole
|
||||||
|
try:
|
||||||
|
tl_pt = Point(float(lon), float(lat), srid=4326)
|
||||||
|
tl_pt.transform(3812)
|
||||||
|
tl_pole = TrafficLightPole.objects.filter(geom__dwithin=(tl_pt, 5)).first()
|
||||||
|
if tl_pole:
|
||||||
|
panel.trafficlight_pole = tl_pole
|
||||||
|
if not panel.support_model and tl_pole.model:
|
||||||
|
panel.support_model = tl_pole.model.get_name()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# If coordinates are provided and geom not yet set
|
# If coordinates are provided and geom not yet set
|
||||||
if lat and lon and not panel.geom:
|
if lat and lon and not panel.geom:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue