# ---------------------- # Road Sign Models # ---------------------- from django.contrib.gis.db import models from django.urls import reverse from django.utils.text import slugify from django.utils.translation import get_language from django.conf import settings from django.utils.translation import gettext_lazy as _ import os from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel from common.models import Municipality, Document # Road Sign Street class SignStreet(AbstractLocation): geom = models.MultiPolygonField(srid=3812, null=True,blank=True) municipality = models.ForeignKey(Municipality, on_delete=models.SET_NULL, null=True, blank=True, related_name="sign_streets") road_code = models.CharField(max_length=50, null=True, blank=True) section_code = models.CharField(max_length=50, null=True, blank=True) street_level = models.CharField(max_length=50, null=True, blank=True) def get_active_assets(self): # Retrieve associated signs signpoles = self.signpoles.filter(status='active') signpanels = self.signpanels.filter(status='active') return { "signpoles": signpoles, "signpanels": signpanels, } def get_all_assets(self): # Retrieve associated signs signpoles = self.signpoles.all() signpanels = self.signpanels.all() return { "signpoles": signpoles, "signpanels": signpanels, } def get_direct_child_assets(self): # Retrieve associated signs signpoles = self.signpoles.all() signpanels = self.signpanels.all() return { "signpoles": signpoles, "signpanels": signpanels, } def get_absolute_url(self): return reverse("assets:sign_streets_detail", kwargs={"street_id": self.id}) def upload_to_signstreet(instance, filename): location = instance.street code = slugify(location.code or "unknown") # Fallback si code est None return os.path.join("assets_documents", "sign", code, filename) SIGN_DOCUMENT_TYPE_CHOICES = ( ('as_built', 'As Built'), ('other', 'Autre'), ) class SignStreetDocument(Document): street = models.ForeignKey(SignStreet, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Street") file = models.FileField(upload_to=upload_to_signstreet, verbose_name="Fichier", max_length=255) thumbnail = models.ImageField(upload_to=upload_to_signstreet, null=True, blank=True, max_length=255) document_type = models.CharField(max_length=50, choices=SIGN_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") uploaded_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Ajouté par", related_name="uploaded_sign_street_documents_assets", ) # Road Sign Pole Model class SignPoleModel(AbstractAssetModel): color = models.CharField(max_length=200, null=True,blank=True) ral = models.CharField(max_length=200, null=True,blank=True) height = models.FloatField(null=True,blank=True) diameter = models.FloatField(null=True,blank=True) material = models.CharField(max_length=200, null=True,blank=True) # Road Sign Pole class SignPole(AbstractGeoAsset): model = models.ForeignKey(SignPoleModel, on_delete=models.PROTECT, null=True, blank=True) street = models.ForeignKey(SignStreet, related_name="signpoles", on_delete=models.PROTECT) orientation = models.IntegerField( null=True, blank=True, default=90, help_text="Orientation en degrés (0-360)" ) class Meta: indexes = [ models.Index(fields=["code"]), ] def get_absolute_url(self): return reverse('assets:sign_assets_detail', kwargs={'asset_model': 'signpole', 'asset_id':self.id}) # Road Sign Panel Model class SignPanelModel(AbstractAssetModel): shape = models.CharField(max_length=200, null=True,blank=True) color = models.CharField(max_length=200, null=True,blank=True) ral = models.CharField(max_length=200, null=True,blank=True) height = models.FloatField(null=True,blank=True) width = models.FloatField(null=True,blank=True) diameter = models.FloatField(null=True,blank=True) material = models.CharField(max_length=200, null=True,blank=True) # Road Sign Panel Type class SignPanelType(models.Model): code = models.CharField(max_length=100, unique=True) name_fr = models.CharField(max_length=200) name_nl = models.CharField(max_length=200) description = models.TextField(null=True, blank=True) sgv_picture = models.ImageField(null=True, blank=True, upload_to="sign/signpanels_types/") default_width = models.FloatField(null=True, blank=True, verbose_name="Largeur par défaut (cm)") default_height = models.FloatField(null=True, blank=True, verbose_name="Hauteur par défaut (cm)") default_diameter = models.FloatField(null=True, blank=True, verbose_name="Diamètre par défaut (cm)") def get_name(self): lang = get_language() name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr return name def __str__(self): 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'), ) # Road Sign Panel class SignPanel(AbstractGeoAsset): 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) pole = models.ForeignKey(SignPole, related_name="signpanels", on_delete=models.SET_NULL, 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_value = models.FloatField(null=True, blank=True) 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) is_temporary = models.BooleanField(default=False) street = models.ForeignKey(SignStreet, related_name="signpanels", on_delete=models.SET_NULL, null=True, blank=True) trafficlight_pole = models.ForeignKey("assets.TrafficLightPole", related_name="signpanels_attached", on_delete=models.SET_NULL, null=True, blank=True) publiclighting_pole = models.ForeignKey("assets.PublicLightingPole", related_name="signpanels_attached", on_delete=models.SET_NULL, null=True, blank=True) class Meta: indexes = [ models.Index(fields=["code"]), ] verbose_name = _("Panneau de signalisation") verbose_name_plural = _("Panneaux de signalisation") def get_absolute_url(self): return reverse('assets:sign_assets_detail', kwargs={'asset_model': 'signpanel', 'asset_id':self.id}) def get_width(self): """Retourne la largeur du panneau en cm (50 cm par défaut ou valeur par défaut du type de panneau)""" width = getattr(self, 'width', None) if width: return width model = getattr(self, 'model', None) if model: w = getattr(model, 'width', None) or getattr(model, 'diameter', None) if w: return w if self.signpanel_type: if self.signpanel_type.default_width: return self.signpanel_type.default_width if self.signpanel_type.default_diameter: return self.signpanel_type.default_diameter return 50.0 # Valeur par défaut def get_height(self): """Retourne la hauteur du panneau en cm (égale à la largeur par défaut ou valeur par défaut du type de panneau)""" height = getattr(self, 'height', None) if height: return height model = getattr(self, 'model', None) if model: h = getattr(model, 'height', None) if h: return h if self.signpanel_type: if self.signpanel_type.default_height: return self.signpanel_type.default_height if self.signpanel_type.default_diameter: return self.signpanel_type.default_diameter return self.get_width() # Hauteur = largeur par défaut def get_scale_factor(self): """Retourne le facteur d'échelle par rapport à la taille de référence (50 cm)""" return self.get_width() / 50.0 def upload_to_police_regulation(instance, filename): code = slugify(instance.reference or instance.title or "regulation") return os.path.join("assets_documents", "sign", "police_regulations", code, filename) class ComplementaryPoliceRegulation(Document): title = models.CharField(max_length=255) reference = models.CharField(max_length=100, null=True, blank=True) decision_date = models.DateField(null=True, blank=True) file = models.FileField(upload_to=upload_to_police_regulation, verbose_name="Fichier", max_length=255) thumbnail = models.ImageField(upload_to=upload_to_police_regulation, null=True, blank=True, max_length=255) panels = models.ManyToManyField(SignPanel, related_name="complementary_regulations", blank=True) uploaded_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Ajouté par", related_name="uploaded_complementary_police_regulations_assets", ) class Meta: ordering = ["-decision_date", "reference", "title"] def __str__(self): ref = f"{self.reference} - " if self.reference else "" return f"{ref}{self.title}" SPOT_TYPE_CHOICES = ( ('car_parking', 'Place de stationnement'), ('bicycle_parking', 'Place de stationnement pour vélos'), ('loading_zone', 'Zone de chargement/déchargement'), ('bus_stop', 'Arrêt de bus'), ('taxi_stand', 'Station de taxis'), ('other', 'Autre'), ) class SignParkingSpot(AbstractGeoAsset): geom = models.PolygonField(srid=3812, null=True, blank=True) street = models.ForeignKey(SignStreet, related_name="parking_spots", on_delete=models.SET_NULL, null=True, blank=True) spot_type = models.CharField(max_length=50, choices=SPOT_TYPE_CHOICES, default='other', verbose_name="Type de place") capacity = models.IntegerField(null=True, blank=True, help_text="Capacité de la place (ex: nombre de véhicules pouvant stationner)") is_temporary = models.BooleanField(default=False, help_text="Indique si la place est temporaire (ex: zone de chantier)") poles = models.ManyToManyField(SignPole, related_name="parking_spots_attached", blank=True) panels = models.ManyToManyField(SignPanel, related_name="parking_spots_attached", blank=True) entity = models.CharField(max_length=255, null=True, blank=True, help_text="Entité responsable de la place (ex: société de parking)") def get_absolute_url(self): return reverse('assets:sign_assets_detail', kwargs={'asset_model': 'parking_spot', 'asset_id':self.id}) def __str__(self): return f"{self.get_spot_type_display()}{' - ' + self.get_name() if self.get_name() else ''}"